mnemolith 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mnemolith-0.2.0/.gitignore +22 -0
- mnemolith-0.2.0/LICENSE +21 -0
- mnemolith-0.2.0/PKG-INFO +424 -0
- mnemolith-0.2.0/README.md +409 -0
- mnemolith-0.2.0/docs/README.md +41 -0
- mnemolith-0.2.0/legacy/aether-cache/LICENSE +21 -0
- mnemolith-0.2.0/legacy/aether-cache/README.md +22 -0
- mnemolith-0.2.0/legacy/aether-cache/pyproject.toml +25 -0
- mnemolith-0.2.0/mnemolith/__init__.py +3 -0
- mnemolith-0.2.0/mnemolith/__main__.py +5 -0
- mnemolith-0.2.0/mnemolith/cli.py +462 -0
- mnemolith-0.2.0/mnemolith/config.py +51 -0
- mnemolith-0.2.0/mnemolith/connect.py +292 -0
- mnemolith-0.2.0/mnemolith/engine/mnemolith-engine.jar +0 -0
- mnemolith-0.2.0/mnemolith/engine.py +356 -0
- mnemolith-0.2.0/mnemolith/javart.py +183 -0
- mnemolith-0.2.0/mnemolith/legacy.py +68 -0
- mnemolith-0.2.0/mnemolith/portability.py +411 -0
- mnemolith-0.2.0/mnemolith/repos.py +140 -0
- mnemolith-0.2.0/mnemolith/server.py +530 -0
- mnemolith-0.2.0/mnemolith/ui/fonts/OFL.txt +93 -0
- mnemolith-0.2.0/mnemolith/ui/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- mnemolith-0.2.0/mnemolith/ui/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- mnemolith-0.2.0/mnemolith/ui/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- mnemolith-0.2.0/mnemolith/ui/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- mnemolith-0.2.0/mnemolith/ui/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- mnemolith-0.2.0/mnemolith/ui/index.html +1236 -0
- mnemolith-0.2.0/mnemolith/ui.py +497 -0
- mnemolith-0.2.0/pyproject.toml +37 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
*.jar
|
|
2
|
+
!mnemolith/engine/mnemolith-engine.jar
|
|
3
|
+
target/
|
|
4
|
+
.idea/
|
|
5
|
+
src/main/java/embeddings_cache.npz
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# AI tool and design-hook config (Impeccable etc.), kept local
|
|
12
|
+
.claude/
|
|
13
|
+
.agent/
|
|
14
|
+
.agents/
|
|
15
|
+
.codex/
|
|
16
|
+
.cursor/
|
|
17
|
+
.gemini/
|
|
18
|
+
.kiro/
|
|
19
|
+
.impeccable/
|
|
20
|
+
PRODUCT.md
|
|
21
|
+
DESIGN.md
|
|
22
|
+
.DS_Store
|
mnemolith-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anish Chugh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mnemolith-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mnemolith
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared memory for AI coding tools that stays true to your code.
|
|
5
|
+
Project-URL: Homepage, https://mnemolith.dev/
|
|
6
|
+
Project-URL: Repository, https://github.com/anishchugh05/mnemolith
|
|
7
|
+
Author: Anish Chugh
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: fastembed>=0.3
|
|
12
|
+
Requires-Dist: mcp>=1.2
|
|
13
|
+
Requires-Dist: redis>=5.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
[](https://mnemolith.dev/)
|
|
17
|
+
|
|
18
|
+
# Mnemolith
|
|
19
|
+
|
|
20
|
+
**Shared memory for AI coding tools that stays true to your code.**
|
|
21
|
+
|
|
22
|
+
*Mnemolith (NEE-mo-lith): "memory stone." What your AI learns, carved where every tool can read it. ([Why the name](https://mnemolith.dev/about.html#name))*
|
|
23
|
+
|
|
24
|
+
> **Formerly Aether Cache.** If you installed `aether-cache`, see [Moving from Aether Cache](#moving-from-aether-cache): your memories come with you.
|
|
25
|
+
|
|
26
|
+
<!-- mcp-name: io.github.anishchugh05/mnemolith -->
|
|
27
|
+
|
|
28
|
+
**[Website](https://mnemolith.dev/)** · **[PyPI](https://pypi.org/project/mnemolith/)**
|
|
29
|
+
|
|
30
|
+
Claude Code, Cursor, and other AI tools each forget your project between sessions, and when they do remember things,
|
|
31
|
+
nothing checks whether those memories are still correct. Mnemolith gives every tool one shared, local memory, and keeps
|
|
32
|
+
it honest:
|
|
33
|
+
|
|
34
|
+
- **Memories go stale when the code changes.** Each memory is linked to the files it's about, with a snapshot of their
|
|
35
|
+
git content hash. When `docker-compose.yml` changes, the memory about it is retired automatically instead of being
|
|
36
|
+
fed to your AI forever.
|
|
37
|
+
- **Contradictions get replaced, not hoarded.** "We migrated to MySQL" retires "we use Postgres" instead of sitting
|
|
38
|
+
next to it and confusing the model. Near-identical repeats are merged.
|
|
39
|
+
- **Every memory can explain itself.** Which tool and session saved it, which file versions it was based on, and every
|
|
40
|
+
change since: created, seen again, went stale, confirmed, replaced.
|
|
41
|
+
- **It works with the files you already use.** Current memories sync into a managed section of `AGENTS.md` and
|
|
42
|
+
`CLAUDE.md`, so they're committed, reviewed in PRs, and visible to teammates who don't run Mnemolith.
|
|
43
|
+
|
|
44
|
+
It runs entirely on your machine, on top of a database engine I wrote from scratch.
|
|
45
|
+
|
|
46
|
+
## Not a Redis clone
|
|
47
|
+
|
|
48
|
+
Mnemolith started as a from-scratch rebuild of Redis in Java, then kept going. Every layer here is written from
|
|
49
|
+
scratch, and each one does something the layer beneath it can't:
|
|
50
|
+
|
|
51
|
+
| Layer | What it adds |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **MCP server** | Claude Code, Cursor, and any MCP tool share one memory |
|
|
54
|
+
| **Git awareness** | Memories retire themselves when the code they describe changes |
|
|
55
|
+
| **Memory layer** | Status, provenance, full history, duplicate merging, and conflict review, inside the engine |
|
|
56
|
+
| **HNSW vector index** | Semantic search: 99.4% recall at 0.84 ms on 100K vectors, 22× faster than exact search |
|
|
57
|
+
| **Redis-compatible engine** | RESP protocol, 50+ commands, replication, transactions, and crash-safe persistence (0 acknowledged writes lost across 1,039,890 writes and 8 `kill -9` crashes), at 88–93% of real Redis's throughput |
|
|
58
|
+
|
|
59
|
+
The bottom layer alone is a working Redis: `redis-cli`, `redis-benchmark`, and `redis-py` talk to it unchanged. The
|
|
60
|
+
memory layer, git awareness, and MCP server are things Redis doesn't do at all.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Claude Code ─┐ ┌─────────────────────────────────────────────┐
|
|
64
|
+
Cursor ──────┼─ MCP ─► mnemolith ─┼─ RESP ─► Java engine (Main.java) │
|
|
65
|
+
other tools ─┘ (Python: embeddings,│ memories, HNSW vector index, │
|
|
66
|
+
git hashing, sync) │ history, append-only file on disk │
|
|
67
|
+
└─────────────────────────────────────────────┘
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What it looks like
|
|
71
|
+
|
|
72
|
+
The dashboard (`mnemolith ui`): memories whose files changed wait for a decision, with the file versions side by side.
|
|
73
|
+
|
|
74
|
+
<picture>
|
|
75
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://mnemolith.dev/assets/dashboard-dark.png">
|
|
76
|
+
<img alt="The Mnemolith dashboard showing stale memories and a possible conflict" src="https://mnemolith.dev/assets/dashboard-light.png">
|
|
77
|
+
</picture>
|
|
78
|
+
|
|
79
|
+
And from your AI tool's side, a memory linked to a file, the file changes, and the next lookup catches it:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
> remember("The database runs Postgres via docker compose", files=["docker-compose.yml"])
|
|
83
|
+
Saved as m1.
|
|
84
|
+
|
|
85
|
+
... someone edits docker-compose.yml ...
|
|
86
|
+
|
|
87
|
+
> recall("database setup")
|
|
88
|
+
These memories were just retired because files they depend on changed. If one is still true,
|
|
89
|
+
call confirm_memory; if it's wrong, save the corrected fact with remember:
|
|
90
|
+
[m1] "The database runs Postgres via docker compose" is now marked outdated: docker-compose.yml changed.
|
|
91
|
+
|
|
92
|
+
> confirm_memory("m1") # still true, so re-link it to the new version of the file
|
|
93
|
+
Confirmed m1; it's active again and linked to the current file versions.
|
|
94
|
+
|
|
95
|
+
> explain_memory("m1")
|
|
96
|
+
m1: "The database runs Postgres via docker compose"
|
|
97
|
+
status: active, project: demo
|
|
98
|
+
saved by: claude-code (session eea54b5c)
|
|
99
|
+
linked files: docker-compose.yml@02ea346...
|
|
100
|
+
history:
|
|
101
|
+
19:16 created: by claude-code (session eea54b5c), linked to docker-compose.yml@87da641...
|
|
102
|
+
19:16 active -> stale: docker-compose.yml changed since this was saved
|
|
103
|
+
19:16 re-linked: to docker-compose.yml@02ea346... (confirmed still true)
|
|
104
|
+
19:16 stale -> active: confirmed still true
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A newer fact replacing an older one:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
> remember("The API timeout was raised to 60 seconds")
|
|
111
|
+
Saved as m3.
|
|
112
|
+
Related existing memories:
|
|
113
|
+
[m1] (similarity 0.58) The API timeout is 30 seconds
|
|
114
|
+
If m3 makes any of these no longer true, call replace_memory(old_id, "m3"). Otherwise do nothing.
|
|
115
|
+
|
|
116
|
+
> replace_memory("m1", "m3")
|
|
117
|
+
m1 retired; m3 replaces it.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Quick start
|
|
121
|
+
|
|
122
|
+
One line installs everything and runs setup. On macOS or Linux:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
curl -fsSL https://mnemolith.dev/install.sh | sh
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
On Windows (PowerShell):
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
powershell -ExecutionPolicy ByPass -c "irm https://mnemolith.dev/install.ps1 | iex"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The installer ([install.sh](docs/install.sh), [install.ps1](docs/install.ps1)) gets [uv](https://docs.astral.sh/uv/) if you
|
|
135
|
+
don't have it (uv brings Python), installs Mnemolith, and runs `mnemolith setup`. Prefer your own tools? Any of
|
|
136
|
+
these works, followed by `mnemolith setup`:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
uv tool install mnemolith
|
|
140
|
+
pipx install mnemolith
|
|
141
|
+
pip install mnemolith # Python 3.10+
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`setup` starts the engine in the background, downloads the embedding model once (about 90 MB), and connects every
|
|
145
|
+
supported AI tool it finds on your machine: **Claude Code, Cursor, and Codex**. That's it. Open your AI tool in any git
|
|
146
|
+
repo and try:
|
|
147
|
+
|
|
148
|
+
> Remember that our tests need DOCKER_HOST set. It depends on docker-compose.yml.
|
|
149
|
+
|
|
150
|
+
Then start a new session and ask how to run the tests.
|
|
151
|
+
|
|
152
|
+
The engine runs on Java 11+. If you have it, it's used. If you don't, Mnemolith downloads a private copy (about
|
|
153
|
+
50 MB) into `~/.mnemolith/java`, checks it against its published checksum, and uses it only for Mnemolith;
|
|
154
|
+
nothing is installed system-wide, and `mnemolith uninstall` removes it.
|
|
155
|
+
|
|
156
|
+
Already keep notes for your AI in `CLAUDE.md`, `AGENTS.md`, or Cursor rules? Run `mnemolith import` in the repo
|
|
157
|
+
(or use **Import notes** in the dashboard) to turn them into memories, linked to the files they mention.
|
|
158
|
+
|
|
159
|
+
Memories are kept per project (the git repo's folder name), so projects never mix. Everything stays on your machine:
|
|
160
|
+
the engine only accepts connections from it, and your memories live in `~/.mnemolith`.
|
|
161
|
+
|
|
162
|
+
| Command | What it does |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `mnemolith setup` | One-time setup: engine, model, and AI tools |
|
|
165
|
+
| `mnemolith add` | Adds the repo you're in (or a path) so the dashboard can check its files; the dashboard can also find and add repos for you |
|
|
166
|
+
| `mnemolith ui` | Opens the dashboard: see what your AI remembers, and confirm or retire memories whose files changed |
|
|
167
|
+
| `mnemolith import` | Turns the notes in `CLAUDE.md`, `AGENTS.md`, and Cursor rules into memories; shows them first and asks |
|
|
168
|
+
| `mnemolith export` | Saves memories, with their linked files and history, to a JSON file (`-p` for one project) |
|
|
169
|
+
| `mnemolith status` | Is it running, and what's in this project's memory |
|
|
170
|
+
| `mnemolith doctor` | Checks everything and tells you how to fix what's wrong |
|
|
171
|
+
| `mnemolith list` / `recall "query"` / `explain m1` | Look at memories from the terminal |
|
|
172
|
+
| `mnemolith sync` | Writes current memories into `AGENTS.md` / `CLAUDE.md` |
|
|
173
|
+
| `mnemolith connect cursor` | Connects one tool (`--print` shows config for any other MCP tool) |
|
|
174
|
+
| `mnemolith start` / `stop` | Runs or stops the engine (it also starts on its own when a tool needs it) |
|
|
175
|
+
| `mnemolith uninstall` | Disconnects your AI tools, stops the engine, and asks before deleting memories |
|
|
176
|
+
|
|
177
|
+
If `mnemolith` isn't found after installing, run `uv tool update-shell` and open a new terminal. Port 7380 taken?
|
|
178
|
+
Set `MNEMOLITH_PORT` and run `setup` again. To uninstall: `mnemolith uninstall`, then `uv tool uninstall mnemolith`.
|
|
179
|
+
|
|
180
|
+
## Moving from Aether Cache
|
|
181
|
+
|
|
182
|
+
Mnemolith used to be called Aether Cache. If you installed `aether-cache`, run the installer above. It installs
|
|
183
|
+
Mnemolith, removes `aether-cache`, moves your memories from `~/.aether-cache` to `~/.mnemolith`, and reconnects your AI
|
|
184
|
+
tools. Nothing is lost, and memories synced into `CLAUDE.md` / `AGENTS.md` are picked up where they are.
|
|
185
|
+
|
|
186
|
+
Installed some other way? Install `mnemolith` the same way, run `mnemolith setup`, then remove `aether-cache`
|
|
187
|
+
(`uv tool uninstall aether-cache`, `pipx uninstall aether-cache`, or `pip uninstall aether-cache`).
|
|
188
|
+
|
|
189
|
+
## Tools the AI can call
|
|
190
|
+
|
|
191
|
+
| Tool | What it does |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `remember(fact, files?)` | Saves a durable fact, optionally linked to files. Merges near-identical repeats and shows related memories so the AI can decide whether the new fact replaces one. |
|
|
194
|
+
| `recall(query, limit?)` | Searches the project's memories by meaning. Checks linked files first, so it never returns a memory whose code changed underneath it. |
|
|
195
|
+
| `explain_memory(id)` | Where a memory came from and its full history. |
|
|
196
|
+
| `replace_memory(old, new)` | Retires an older memory that a newer one makes untrue. |
|
|
197
|
+
| `mark_outdated(id, reason)` | Retires a memory that turned out to be wrong. |
|
|
198
|
+
| `confirm_memory(id)` | Brings back a memory that's still true after its files changed, re-linked to the current versions. |
|
|
199
|
+
| `check_stale()` | Runs the file check on demand. |
|
|
200
|
+
| `list_conflicts()` / `resolve_conflict(...)` | Reviews pairs of very similar memories that may contradict each other. |
|
|
201
|
+
| `sync_to_files()` | Writes current memories into `AGENTS.md` / `CLAUDE.md` (also: `mnemolith sync`). |
|
|
202
|
+
|
|
203
|
+
## Design decisions, and what the measurements said
|
|
204
|
+
|
|
205
|
+
### Similarity can't detect contradictions, so the AI judges
|
|
206
|
+
|
|
207
|
+
The first version flagged a new memory as a possible contradiction when it was similar to an existing one. Calibrating
|
|
208
|
+
with real `all-MiniLM-L6-v2` embeddings showed that can't work:
|
|
209
|
+
|
|
210
|
+
| Pair | Kind | Similarity |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| "we use Postgres" / "the database is Postgres" | duplicate | 0.73 |
|
|
213
|
+
| "tests need DOCKER_HOST set" / "you have to set DOCKER_HOST before running tests" | duplicate | 0.92 |
|
|
214
|
+
| "we use Postgres" / "we migrated to MySQL" | **contradiction** | **0.30** |
|
|
215
|
+
| "API timeout is 30 seconds" / "API timeout is 60 seconds" | **contradiction** | **0.94** |
|
|
216
|
+
| "tests need DOCKER_HOST set" / "integration tests need a running Redis" | related, both true | 0.34 |
|
|
217
|
+
|
|
218
|
+
Duplicates scored 0.73 to 0.92 and contradictions 0.30 to 0.94, so no threshold separates them. A threshold low enough
|
|
219
|
+
to merge real duplicates would silently merge "timeout is 30s" with "timeout is 60s", the worst bug this product could
|
|
220
|
+
have. So the engine only auto-merges near-identical text (similarity ≥ 0.97). For everything else, embeddings *find*
|
|
221
|
+
the related memories and the AI, which already understands the sentences, *decides* whether one replaces another.
|
|
222
|
+
|
|
223
|
+
### Staleness compares file contents, not commits
|
|
224
|
+
|
|
225
|
+
Each linked file is stored as `path@<git blob hash>`, computed with `git hash-object`. Comparing contents rather than
|
|
226
|
+
commit ids catches uncommitted edits, ignores commits that didn't touch the file, and needs no git hooks: `recall`
|
|
227
|
+
re-hashes the handful of linked files before every search.
|
|
228
|
+
|
|
229
|
+
### Memories never mutate silently
|
|
230
|
+
|
|
231
|
+
Every change is a logged event with a timestamp and a reason. The engine logs each memory *with* the id and timestamp
|
|
232
|
+
it assigned, so replaying the log after a restart rebuilds the exact same memories and histories, down to the
|
|
233
|
+
millisecond, instead of minting new ones.
|
|
234
|
+
|
|
235
|
+
## The engine underneath
|
|
236
|
+
|
|
237
|
+
A Redis-compatible server written from scratch in Java. It speaks RESP, so `redis-cli`, `redis-benchmark`, and
|
|
238
|
+
`redis-py` work with it unchanged. The core was built by completing every stage and extension of the
|
|
239
|
+
[CodeCrafters "Build Your Own Redis"](https://codecrafters.io/challenges/redis) challenge; the vector index,
|
|
240
|
+
persistence hardening, and the memory layer are my own additions.
|
|
241
|
+
|
|
242
|
+
[](https://app.codecrafters.io/users/anishchugh05?r=2qF)
|
|
243
|
+
|
|
244
|
+
### Throughput vs. official Redis
|
|
245
|
+
|
|
246
|
+
`redis-benchmark -t set,get`, 100K requests, 50 parallel clients, 3-byte values, same machine:
|
|
247
|
+
|
|
248
|
+
| | Mnemolith | Redis 8.10.2 | Mnemolith as % of Redis |
|
|
249
|
+
|---|---|---|---|
|
|
250
|
+
| SET | 205.8K ops/sec | 233.6K ops/sec | 88% |
|
|
251
|
+
| GET | 225.7K ops/sec | 242.7K ops/sec | 93% |
|
|
252
|
+
| p50 latency | 0.119 ms | 0.111 ms | |
|
|
253
|
+
| p99 latency (SET / GET) | 0.375 / 0.231 ms | 0.359 / 0.159 ms | |
|
|
254
|
+
| max latency (SET) | 19.7 ms | 1.3 ms | JVM GC / JIT pauses |
|
|
255
|
+
|
|
256
|
+
Typical latency is on par with Redis. The gap is in worst-case latency, from JVM garbage collection and JIT warm-up,
|
|
257
|
+
which Redis (written in C) doesn't have.
|
|
258
|
+
|
|
259
|
+
### HNSW vector search vs. exact search
|
|
260
|
+
|
|
261
|
+
100,000 AG News headlines embedded with `all-MiniLM-L6-v2` (384 dimensions, cosine similarity), 500 held-out queries,
|
|
262
|
+
k = 10. Recall is measured against exact brute-force search on the same data.
|
|
263
|
+
|
|
264
|
+
| Mode | Recall@10 | p50 latency | p99 latency |
|
|
265
|
+
|---|---|---|---|
|
|
266
|
+
| Exact (brute force) | 1.000 | 18.58 ms | 20.52 ms |
|
|
267
|
+
| HNSW, ef=10 | 0.905 | 0.49 ms | 1.38 ms |
|
|
268
|
+
| HNSW, ef=20 | 0.951 | 0.52 ms | 0.73 ms |
|
|
269
|
+
| HNSW, ef=50 | 0.985 | 0.66 ms | 0.89 ms |
|
|
270
|
+
| **HNSW, ef=100** | **0.994** | **0.84 ms** | **1.41 ms** |
|
|
271
|
+
| HNSW, ef=200 | 0.998 | 1.24 ms | 2.19 ms |
|
|
272
|
+
|
|
273
|
+
At ef=100, HNSW returns 99.4% of the true top-10 about **22× faster** than exact search. Building the index for 100K
|
|
274
|
+
vectors took 136 seconds (M=16, M0=32, efConstruction=200). Latencies are measured from the Python client, so they
|
|
275
|
+
include network and client overhead. Full setup and raw numbers: `results.json`.
|
|
276
|
+
|
|
277
|
+
**How it works:** each vector links to its nearest neighbors in a layered graph. A search starts on a sparse top layer
|
|
278
|
+
of long-range links, hops greedily toward the query, drops down a layer at a time, and finishes with a best-first search
|
|
279
|
+
on the bottom layer that keeps the `ef` best candidates. Neighbor selection uses the diversity heuristic from the HNSW
|
|
280
|
+
paper (prefer neighbors in different directions over near-duplicates), which is what keeps recall high.
|
|
281
|
+
|
|
282
|
+
**Filtered search:** HNSW doesn't know about memory status, so searches over-fetch candidates, drop the ones that don't
|
|
283
|
+
match (stale or superseded), widen the search if too few survive, and fall back to an exact scan if needed. In a test
|
|
284
|
+
with 2,000 memories where only 2% were active, filtered searches returned exactly the correct top 10 on 30/30 queries.
|
|
285
|
+
The catch: at that selectivity, the exact fallback runs most of the time, which is the known weak spot of
|
|
286
|
+
post-filtering on graph indexes.
|
|
287
|
+
|
|
288
|
+
### Crash safety
|
|
289
|
+
|
|
290
|
+
Every write goes to an append-only file (AOF) that is replayed on startup.
|
|
291
|
+
|
|
292
|
+
**`kill -9` crash test** (`crash_test.py`): a client writes as fast as it can, the server is killed at a random moment,
|
|
293
|
+
restarted, and every write the server acknowledged is checked.
|
|
294
|
+
|
|
295
|
+
| fsync policy | Crashes | Acknowledged writes | Lost |
|
|
296
|
+
|---|---|---|---|
|
|
297
|
+
| `everysec` | 5 | 757,295 | **0** |
|
|
298
|
+
| `always` | 3 | 282,595 | **0** |
|
|
299
|
+
|
|
300
|
+
Recovery took 0.6 seconds per restart, replaying up to ~185K writes.
|
|
301
|
+
|
|
302
|
+
**Torn writes:** a machine crash can cut the last write off halfway. The first version refused to start on such a file;
|
|
303
|
+
now replay stops at the last complete command, trims the partial one, and starts normally with all earlier data intact.
|
|
304
|
+
|
|
305
|
+
**Cost of each fsync policy** (SET throughput, 50 clients):
|
|
306
|
+
|
|
307
|
+
| Policy | Throughput | p50 latency |
|
|
308
|
+
|---|---|---|
|
|
309
|
+
| `always` (sync every write) | 42.9K/sec | 1.159 ms |
|
|
310
|
+
| `everysec` (sync once a second) | 113.9K/sec | 0.103 ms |
|
|
311
|
+
| `no` (let the OS decide) | 118.8K/sec | 0.103 ms |
|
|
312
|
+
|
|
313
|
+
`everysec` is 2.7× faster than `always` and within 4% of `no`, which is why it's the sensible default. On macOS, `fsync`
|
|
314
|
+
doesn't force the SSD's own cache to flush, so `always` looks cheaper here than it would on a Linux server.
|
|
315
|
+
|
|
316
|
+
### Full command list
|
|
317
|
+
|
|
318
|
+
- **Memory:**
|
|
319
|
+
- `MEM.ADD`, `MEM.GET`, `MEM.SEARCH` (with `STATUS` filter), `MEM.STATUS`, `MEM.WHY`, `MEM.LIST`
|
|
320
|
+
- `MEM.FILES`, `MEM.SEEN`, `MEM.CONFLICTS`, `MEM.RESOLVE`
|
|
321
|
+
- **Vectors:** `VADD`, `VSEARCH` (HNSW, tunable `EF`, `EXACT` mode)
|
|
322
|
+
- **Strings:** `GET`, `SET` (`EX` / `PX`), `INCR`, `STRLEN`, `TYPE`, `KEYS *`
|
|
323
|
+
- **Lists:** `RPUSH`, `LPUSH`, `LRANGE`, `LLEN`, `LPOP`, blocking `BLPOP`
|
|
324
|
+
- **Streams:** `XADD` (explicit, partial, and auto IDs), `XRANGE`, `XREAD` (with `BLOCK` and `$`)
|
|
325
|
+
- **Sorted sets:** `ZADD`, `ZRANK`, `ZRANGE`, `ZCARD`, `ZSCORE`, `ZREM`
|
|
326
|
+
- **Bitmaps:** `SETBIT`, `GETBIT`, `BITCOUNT`, `BITOP AND` / `OR` (binary-safe strings)
|
|
327
|
+
- **Geospatial:** `GEOADD`, `GEOPOS`, `GEODIST`, `GEOSEARCH ... FROMLONLAT ... BYRADIUS`
|
|
328
|
+
- **Transactions:** `MULTI`, `EXEC`, `DISCARD`, `WATCH`, `UNWATCH` (optimistic locking via per-key write versions)
|
|
329
|
+
- **Pub/Sub:** `SUBSCRIBE`, `UNSUBSCRIBE`, `PUBLISH`
|
|
330
|
+
- **Replication:**
|
|
331
|
+
- Master–replica handshake (`PING`, `REPLCONF`, `PSYNC`) and command propagation
|
|
332
|
+
- `REPLCONF GETACK` offset tracking and `WAIT`
|
|
333
|
+
- **Persistence:** RDB loading; AOF with a manifest, `appendfsync always` / `everysec` / `no`, and torn-write recovery
|
|
334
|
+
- **Auth:** `AUTH`, `ACL WHOAMI` / `GETUSER` / `SETUSER` (SHA-256 password hashes)
|
|
335
|
+
|
|
336
|
+
### Running the engine on its own
|
|
337
|
+
|
|
338
|
+
```sh
|
|
339
|
+
java src/main/java/Main.java # port 6379
|
|
340
|
+
java src/main/java/Main.java --bind 127.0.0.1 # accept connections from this machine only
|
|
341
|
+
java src/main/java/Main.java --port 6380 --replicaof "localhost 6379"
|
|
342
|
+
java src/main/java/Main.java --dir /tmp/data --appendonly yes --appendfsync everysec
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Reproducing the benchmarks
|
|
346
|
+
|
|
347
|
+
Each benchmark runs against the engine you've started:
|
|
348
|
+
|
|
349
|
+
```sh
|
|
350
|
+
redis-benchmark -p 6379 -t set,get # throughput; also run against redis-server to compare
|
|
351
|
+
python vector_bench.py # HNSW recall and latency (start the engine with -Xmx4g)
|
|
352
|
+
python crash_test.py everysec 5 # kill -9 crash test
|
|
353
|
+
python memory_mcp/calibrate.py # embedding similarity calibration
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Related work
|
|
357
|
+
|
|
358
|
+
Mnemolith is an early, open-source project. These are the production systems in the same space:
|
|
359
|
+
|
|
360
|
+
- **Vector search in Redis-style stores:**
|
|
361
|
+
- [Redis 8 vector sets](https://redis.io/docs/latest/develop/data-types/vector-sets/) and
|
|
362
|
+
[valkey-search](https://github.com/valkey-io/valkey-search) add HNSW vector search to Redis and Valkey.
|
|
363
|
+
- [hnswlib](https://github.com/nmslib/hnswlib) is the reference HNSW implementation.
|
|
364
|
+
- [Malkov & Yashunin (2016)](https://arxiv.org/abs/1603.09320) is the paper this index follows.
|
|
365
|
+
- **Memory for AI agents:** [Mem0](https://github.com/mem0ai/mem0), [Zep](https://www.getzep.com/),
|
|
366
|
+
[Letta](https://github.com/letta-ai/letta), and the
|
|
367
|
+
[MCP reference memory server](https://github.com/modelcontextprotocol/servers).
|
|
368
|
+
- **Instruction files:** [AGENTS.md](https://agents.md/) and `CLAUDE.md`. Mnemolith syncs into these rather than
|
|
369
|
+
replacing them.
|
|
370
|
+
|
|
371
|
+
## Status and limitations
|
|
372
|
+
|
|
373
|
+
Mnemolith is v1 and early. It works end to end in automated tests and is now being tried in real Claude Code sessions.
|
|
374
|
+
|
|
375
|
+
**Not done yet:**
|
|
376
|
+
- **A with-memory vs. without-memory evaluation** on real coding tasks, to measure whether it actually reduces
|
|
377
|
+
re-explaining.
|
|
378
|
+
- **AOF compaction:** the log only grows, and every status change adds a line.
|
|
379
|
+
|
|
380
|
+
**Engine limitations:**
|
|
381
|
+
- **Threading:** one OS thread per connection, not an event loop. That's fine for a local tool, but it won't scale to
|
|
382
|
+
thousands of clients like Redis.
|
|
383
|
+
- **Transactions:** `EXEC` isn't isolated, so other clients' commands can interleave with a transaction's commands.
|
|
384
|
+
- **Vector index:** re-adding a vector updates it without rewiring its graph links, and there's no delete. Retired
|
|
385
|
+
memories are filtered out, not removed.
|
|
386
|
+
- **Replication:** full resync only, and a new replica assumes the master's dataset is empty.
|
|
387
|
+
- **Smaller gaps:**
|
|
388
|
+
- `KEYS *` lists only string keys.
|
|
389
|
+
- `GEOSEARCH` scans every member.
|
|
390
|
+
- ACL supports only the `default` user.
|
|
391
|
+
- `HELLO` (RESP3) isn't supported, so clients must use RESP2.
|
|
392
|
+
|
|
393
|
+
## Project layout
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
src/main/java/Main.java the engine (single file, from the CodeCrafters setup)
|
|
397
|
+
mnemolith/ the Python package behind the `mnemolith` command
|
|
398
|
+
server.py MCP server: embeddings, git staleness, AGENTS.md/CLAUDE.md sync
|
|
399
|
+
engine.py finds Java, starts and stops the engine in the background
|
|
400
|
+
javart.py downloads a private copy of Java when the computer has none
|
|
401
|
+
connect.py registers Mnemolith with Claude Code, Cursor, and Codex
|
|
402
|
+
cli.py the `mnemolith` command
|
|
403
|
+
ui.py, ui/index.html the dashboard (`mnemolith ui`): a local server, its API, and the page
|
|
404
|
+
portability.py import from notes files, export to JSON
|
|
405
|
+
engine/mnemolith-engine.jar the prebuilt engine (rebuild with `python scripts/build_engine.py`)
|
|
406
|
+
memory_mcp/calibrate.py similarity calibration for duplicate/contradiction thresholds
|
|
407
|
+
vector_bench.py HNSW benchmark (writes results.json)
|
|
408
|
+
crash_test.py kill -9 crash-safety test
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Releasing
|
|
412
|
+
|
|
413
|
+
Bump the version in `pyproject.toml`, `mnemolith/__init__.py`, and `server.json`, commit, then:
|
|
414
|
+
|
|
415
|
+
```sh
|
|
416
|
+
git tag v0.2.0 && git push --tags
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The [release workflow](.github/workflows/release.yml) checks the versions match, builds and smoke-tests the package,
|
|
420
|
+
publishes it to PyPI and the MCP Registry, and creates the GitHub Release.
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT. See [LICENSE](LICENSE).
|