mnemo-engine 0.4.0__tar.gz → 0.5.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.
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/PKG-INFO +71 -1
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/README.md +70 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/Cargo.lock +101 -4
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/Cargo.toml +22 -2
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/README.md +76 -5
- mnemo_engine-0.5.0/mnemo/deny.toml +131 -0
- mnemo_engine-0.5.0/mnemo/examples/embedder_server.py +51 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/bin/mnemo.rs +273 -15
- mnemo_engine-0.5.0/mnemo/src/embed.rs +280 -0
- mnemo_engine-0.5.0/mnemo/src/embed_cmd.rs +572 -0
- mnemo_engine-0.5.0/mnemo/src/lib.rs +162 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/mcp.rs +284 -82
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/memory.rs +13 -1
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/pager.rs +72 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/store.rs +159 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/wal.rs +25 -4
- mnemo_engine-0.5.0/mnemo/tests/cli_smoke.rs +1336 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/tests/integration.rs +370 -10
- mnemo_engine-0.5.0/mnemo/tests/proptest_store.proptest-regressions +8 -0
- mnemo_engine-0.5.0/mnemo/tests/proptest_store.rs +526 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/Cargo.lock +4 -4
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/Cargo.toml +1 -1
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/README.md +70 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/src/lib.rs +416 -10
- mnemo_engine-0.5.0/mnemo-python/test_mnemo.py +575 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/pyproject.toml +1 -1
- mnemo_engine-0.4.0/mnemo/src/lib.rs +0 -80
- mnemo_engine-0.4.0/mnemo/tests/cli_smoke.rs +0 -553
- mnemo_engine-0.4.0/mnemo-python/test_mnemo.py +0 -240
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/.gitignore +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/examples/quickstart.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/examples/session.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/cache.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/crypto.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/error.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/format.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/index.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/result_cache.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/session.rs +0 -0
- {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/.gitignore +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mnemo-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -155,11 +155,26 @@ its own agent.
|
|
|
155
155
|
| `cache_stats(namespace=None)` | Result-cache stats: `{entries, bytes, hits, misses, hit_rate, evictions}` |
|
|
156
156
|
| `cache_put_semantic(namespace, key, vector, value, model, content_type="text", ttl_secs=None)` | Semantic cache put (Phase 10.2) — vector must match db dimensions |
|
|
157
157
|
| `cache_get_semantic(namespace, query, model, threshold=0.97)` | Top-1 cosine over the namespace's vectored entries whose `model` matches |
|
|
158
|
+
| `set_cache_flush_policy(policy, max_dirty=None, max_age_secs=None)` | `policy="strict"` (default; every put durable on return) or `"batched"` (engine auto-flushes at `max_dirty` puts or `max_age_secs` seconds). Under `batched`, `cache_put` and friends no longer force an eager flush — call `db.flush()` yourself if you need it before a specific boundary |
|
|
159
|
+
| `set_cache_budget(namespace, max_entries, max_bytes)` | Per-namespace LRU cap (defaults 10 000 entries / 64 MiB). On the next `cache_put` the LRU is evicted until both limits are satisfied |
|
|
158
160
|
| `set_max_snapshots(max)` | Override the snapshot-manifest retention cap (default 256; `0` disables) |
|
|
161
|
+
| `memories()` | Enumerate every live (non-deleted, non-expired) memory as a list of dicts |
|
|
162
|
+
| `dimensions()` | Embedding dimensionality this database was created with (also on `stats()["dimensions"]`) |
|
|
163
|
+
| `rekey(new_passphrase, fast=False)` | Change the passphrase. Re-derives the KEK and re-wraps the DEK; encrypted pages are not rewritten (O(1) in db size). `fast=True` uses the cheap-Argon2 test params — omit for production |
|
|
164
|
+
| `set_embedder(fn, *, name, dimensions, normalize=True)` | Register a Python callable `embed(texts: list[str]) -> list[list[float]]` as this handle's text embedder (Phase 3). Dimension-checks against the database's width; identity-checks against (and on first use upserts) the scaffold manifest's `embedder_name` / `embedder_dimensions` / `embedder_normalize` keys, so swapping embedders between opens fails loudly |
|
|
165
|
+
| `remember_text(content, memory_type, **kwargs)` | Embed `content` via the registered embedder and `remember` the result. Accepts the same per-memory kwargs as `remember` (`agent_id`, `importance`, `session_id`, `ttl_secs`, `shared`, `metadata`) |
|
|
166
|
+
| `recall_text(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Embed `query` via the registered embedder and run `recall`. Same return shape as `recall` |
|
|
167
|
+
| `cache_get_semantic_text(namespace, text, threshold=0.97)` | Text-side semantic cache lookup; uses the embedder's `name` as the semantic cache's `model` component |
|
|
159
168
|
| `stats()` | Summary statistics |
|
|
160
169
|
| `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
|
|
161
170
|
| `len(db)` | Live memory count |
|
|
162
171
|
|
|
172
|
+
Plus one module-level function:
|
|
173
|
+
|
|
174
|
+
| Function | Purpose |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `mnemo.compact_file(path, passphrase)` | Rewrite the file out-of-place, dropping tombstoned and expired memories and reclaiming stale pages. Caller's own handle must be closed first. Returns `{"before": int, "after": int}` |
|
|
177
|
+
|
|
163
178
|
`Session` methods: `add_turn(turn)`, `recall(query, top_k=10, memory_types=None)`,
|
|
164
179
|
`close()`, `discard()`, `id()`, `agent()`, `turn_ids()`, `turn_count()`; also a
|
|
165
180
|
context manager (exiting consolidates).
|
|
@@ -359,6 +374,61 @@ your embedder's output — `text-embedding-3-small` is 1536-dim,
|
|
|
359
374
|
consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
|
|
360
375
|
optimized for a single-host agent that owns its cache file.
|
|
361
376
|
|
|
377
|
+
### Recipe: `set_embedder` + text-only `remember` / `recall`
|
|
378
|
+
|
|
379
|
+
Phase 3 ships a per-handle embedder hook so callers never have to
|
|
380
|
+
pass vectors by hand. Register once at open time; every subsequent
|
|
381
|
+
`remember_text` / `recall_text` / `cache_get_semantic_text` call on
|
|
382
|
+
the handle uses it.
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
import mnemo
|
|
386
|
+
from sentence_transformers import SentenceTransformer
|
|
387
|
+
|
|
388
|
+
model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim, normalized
|
|
389
|
+
|
|
390
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
391
|
+
return model.encode(texts, normalize_embeddings=True).tolist()
|
|
392
|
+
|
|
393
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=384)
|
|
394
|
+
db.set_embedder(embed, name="all-MiniLM-L6-v2",
|
|
395
|
+
dimensions=384, normalize=True)
|
|
396
|
+
|
|
397
|
+
db.remember_text("the user prefers tea", "semantic",
|
|
398
|
+
importance=0.8, agent_id="assistant")
|
|
399
|
+
hits = db.recall_text("how does the user like their drinks?", top_k=5)
|
|
400
|
+
for h in hits:
|
|
401
|
+
print(h["score"], h["content"])
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Or with OpenAI's hosted embeddings:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from openai import OpenAI
|
|
408
|
+
client = OpenAI()
|
|
409
|
+
|
|
410
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
411
|
+
r = client.embeddings.create(model="text-embedding-3-small", input=texts)
|
|
412
|
+
return [item.embedding for item in r.data]
|
|
413
|
+
|
|
414
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=1536)
|
|
415
|
+
db.set_embedder(embed, name="text-embedding-3-small",
|
|
416
|
+
dimensions=1536, normalize=False)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The identity (`name`, `dimensions`, `normalize`) is persisted on the
|
|
420
|
+
scaffold manifest the first time you `set_embedder`. Reopening with a
|
|
421
|
+
*different* embedder — say you swap MiniLM for BGE on the same file —
|
|
422
|
+
raises `RuntimeError: embedder identity mismatch: database manifest
|
|
423
|
+
records embedder (name="all-MiniLM-L6-v2", …), but this handle was
|
|
424
|
+
passed embedder (name="bge-large-en-v1.5", …). Mixing embedders
|
|
425
|
+
silently corrupts recall quality — open the file with the original
|
|
426
|
+
embedder, or create a fresh database for the new one.`
|
|
427
|
+
|
|
428
|
+
Mnemo also now refuses any `remember` call with an all-zero vector,
|
|
429
|
+
so a mis-wired embedder (returning `[0.0] * dims`) fails loudly on
|
|
430
|
+
the first write instead of polluting recall silently.
|
|
431
|
+
|
|
362
432
|
## License
|
|
363
433
|
|
|
364
434
|
Apache-2.0.
|
|
@@ -141,11 +141,26 @@ its own agent.
|
|
|
141
141
|
| `cache_stats(namespace=None)` | Result-cache stats: `{entries, bytes, hits, misses, hit_rate, evictions}` |
|
|
142
142
|
| `cache_put_semantic(namespace, key, vector, value, model, content_type="text", ttl_secs=None)` | Semantic cache put (Phase 10.2) — vector must match db dimensions |
|
|
143
143
|
| `cache_get_semantic(namespace, query, model, threshold=0.97)` | Top-1 cosine over the namespace's vectored entries whose `model` matches |
|
|
144
|
+
| `set_cache_flush_policy(policy, max_dirty=None, max_age_secs=None)` | `policy="strict"` (default; every put durable on return) or `"batched"` (engine auto-flushes at `max_dirty` puts or `max_age_secs` seconds). Under `batched`, `cache_put` and friends no longer force an eager flush — call `db.flush()` yourself if you need it before a specific boundary |
|
|
145
|
+
| `set_cache_budget(namespace, max_entries, max_bytes)` | Per-namespace LRU cap (defaults 10 000 entries / 64 MiB). On the next `cache_put` the LRU is evicted until both limits are satisfied |
|
|
144
146
|
| `set_max_snapshots(max)` | Override the snapshot-manifest retention cap (default 256; `0` disables) |
|
|
147
|
+
| `memories()` | Enumerate every live (non-deleted, non-expired) memory as a list of dicts |
|
|
148
|
+
| `dimensions()` | Embedding dimensionality this database was created with (also on `stats()["dimensions"]`) |
|
|
149
|
+
| `rekey(new_passphrase, fast=False)` | Change the passphrase. Re-derives the KEK and re-wraps the DEK; encrypted pages are not rewritten (O(1) in db size). `fast=True` uses the cheap-Argon2 test params — omit for production |
|
|
150
|
+
| `set_embedder(fn, *, name, dimensions, normalize=True)` | Register a Python callable `embed(texts: list[str]) -> list[list[float]]` as this handle's text embedder (Phase 3). Dimension-checks against the database's width; identity-checks against (and on first use upserts) the scaffold manifest's `embedder_name` / `embedder_dimensions` / `embedder_normalize` keys, so swapping embedders between opens fails loudly |
|
|
151
|
+
| `remember_text(content, memory_type, **kwargs)` | Embed `content` via the registered embedder and `remember` the result. Accepts the same per-memory kwargs as `remember` (`agent_id`, `importance`, `session_id`, `ttl_secs`, `shared`, `metadata`) |
|
|
152
|
+
| `recall_text(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Embed `query` via the registered embedder and run `recall`. Same return shape as `recall` |
|
|
153
|
+
| `cache_get_semantic_text(namespace, text, threshold=0.97)` | Text-side semantic cache lookup; uses the embedder's `name` as the semantic cache's `model` component |
|
|
145
154
|
| `stats()` | Summary statistics |
|
|
146
155
|
| `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
|
|
147
156
|
| `len(db)` | Live memory count |
|
|
148
157
|
|
|
158
|
+
Plus one module-level function:
|
|
159
|
+
|
|
160
|
+
| Function | Purpose |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `mnemo.compact_file(path, passphrase)` | Rewrite the file out-of-place, dropping tombstoned and expired memories and reclaiming stale pages. Caller's own handle must be closed first. Returns `{"before": int, "after": int}` |
|
|
163
|
+
|
|
149
164
|
`Session` methods: `add_turn(turn)`, `recall(query, top_k=10, memory_types=None)`,
|
|
150
165
|
`close()`, `discard()`, `id()`, `agent()`, `turn_ids()`, `turn_count()`; also a
|
|
151
166
|
context manager (exiting consolidates).
|
|
@@ -345,6 +360,61 @@ your embedder's output — `text-embedding-3-small` is 1536-dim,
|
|
|
345
360
|
consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
|
|
346
361
|
optimized for a single-host agent that owns its cache file.
|
|
347
362
|
|
|
363
|
+
### Recipe: `set_embedder` + text-only `remember` / `recall`
|
|
364
|
+
|
|
365
|
+
Phase 3 ships a per-handle embedder hook so callers never have to
|
|
366
|
+
pass vectors by hand. Register once at open time; every subsequent
|
|
367
|
+
`remember_text` / `recall_text` / `cache_get_semantic_text` call on
|
|
368
|
+
the handle uses it.
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
import mnemo
|
|
372
|
+
from sentence_transformers import SentenceTransformer
|
|
373
|
+
|
|
374
|
+
model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim, normalized
|
|
375
|
+
|
|
376
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
377
|
+
return model.encode(texts, normalize_embeddings=True).tolist()
|
|
378
|
+
|
|
379
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=384)
|
|
380
|
+
db.set_embedder(embed, name="all-MiniLM-L6-v2",
|
|
381
|
+
dimensions=384, normalize=True)
|
|
382
|
+
|
|
383
|
+
db.remember_text("the user prefers tea", "semantic",
|
|
384
|
+
importance=0.8, agent_id="assistant")
|
|
385
|
+
hits = db.recall_text("how does the user like their drinks?", top_k=5)
|
|
386
|
+
for h in hits:
|
|
387
|
+
print(h["score"], h["content"])
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Or with OpenAI's hosted embeddings:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from openai import OpenAI
|
|
394
|
+
client = OpenAI()
|
|
395
|
+
|
|
396
|
+
def embed(texts: list[str]) -> list[list[float]]:
|
|
397
|
+
r = client.embeddings.create(model="text-embedding-3-small", input=texts)
|
|
398
|
+
return [item.embedding for item in r.data]
|
|
399
|
+
|
|
400
|
+
db = mnemo.open("agent.mnemo", "pw", dimensions=1536)
|
|
401
|
+
db.set_embedder(embed, name="text-embedding-3-small",
|
|
402
|
+
dimensions=1536, normalize=False)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The identity (`name`, `dimensions`, `normalize`) is persisted on the
|
|
406
|
+
scaffold manifest the first time you `set_embedder`. Reopening with a
|
|
407
|
+
*different* embedder — say you swap MiniLM for BGE on the same file —
|
|
408
|
+
raises `RuntimeError: embedder identity mismatch: database manifest
|
|
409
|
+
records embedder (name="all-MiniLM-L6-v2", …), but this handle was
|
|
410
|
+
passed embedder (name="bge-large-en-v1.5", …). Mixing embedders
|
|
411
|
+
silently corrupts recall quality — open the file with the original
|
|
412
|
+
embedder, or create a fresh database for the new one.`
|
|
413
|
+
|
|
414
|
+
Mnemo also now refuses any `remember` call with an all-zero vector,
|
|
415
|
+
so a mis-wired embedder (returning `[0.0] * dims`) fails loudly on
|
|
416
|
+
the first write instead of polluting recall silently.
|
|
417
|
+
|
|
348
418
|
## License
|
|
349
419
|
|
|
350
420
|
Apache-2.0.
|
|
@@ -120,6 +120,21 @@ version = "1.6.0"
|
|
|
120
120
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
121
121
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
|
122
122
|
|
|
123
|
+
[[package]]
|
|
124
|
+
name = "bit-set"
|
|
125
|
+
version = "0.5.3"
|
|
126
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
127
|
+
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
|
128
|
+
dependencies = [
|
|
129
|
+
"bit-vec",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
[[package]]
|
|
133
|
+
name = "bit-vec"
|
|
134
|
+
version = "0.6.3"
|
|
135
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
136
|
+
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
|
137
|
+
|
|
123
138
|
[[package]]
|
|
124
139
|
name = "bitflags"
|
|
125
140
|
version = "2.11.1"
|
|
@@ -293,6 +308,12 @@ version = "2.4.1"
|
|
|
293
308
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
294
309
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
|
295
310
|
|
|
311
|
+
[[package]]
|
|
312
|
+
name = "fnv"
|
|
313
|
+
version = "1.0.7"
|
|
314
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
315
|
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
316
|
+
|
|
296
317
|
[[package]]
|
|
297
318
|
name = "fs4"
|
|
298
319
|
version = "0.9.1"
|
|
@@ -399,6 +420,12 @@ dependencies = [
|
|
|
399
420
|
"wasm-bindgen",
|
|
400
421
|
]
|
|
401
422
|
|
|
423
|
+
[[package]]
|
|
424
|
+
name = "lazy_static"
|
|
425
|
+
version = "1.5.0"
|
|
426
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
427
|
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
428
|
+
|
|
402
429
|
[[package]]
|
|
403
430
|
name = "libc"
|
|
404
431
|
version = "0.2.186"
|
|
@@ -415,6 +442,12 @@ dependencies = [
|
|
|
415
442
|
"windows-link",
|
|
416
443
|
]
|
|
417
444
|
|
|
445
|
+
[[package]]
|
|
446
|
+
name = "libm"
|
|
447
|
+
version = "0.2.16"
|
|
448
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
449
|
+
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
|
450
|
+
|
|
418
451
|
[[package]]
|
|
419
452
|
name = "linux-raw-sys"
|
|
420
453
|
version = "0.4.15"
|
|
@@ -429,13 +462,14 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
429
462
|
|
|
430
463
|
[[package]]
|
|
431
464
|
name = "mnemo-engine"
|
|
432
|
-
version = "0.
|
|
465
|
+
version = "0.5.0"
|
|
433
466
|
dependencies = [
|
|
434
467
|
"aes-gcm",
|
|
435
468
|
"argon2",
|
|
436
469
|
"base64ct",
|
|
437
470
|
"clap",
|
|
438
471
|
"fs4",
|
|
472
|
+
"proptest",
|
|
439
473
|
"rand",
|
|
440
474
|
"rmp",
|
|
441
475
|
"rmp-serde",
|
|
@@ -451,7 +485,7 @@ dependencies = [
|
|
|
451
485
|
|
|
452
486
|
[[package]]
|
|
453
487
|
name = "mnemo-node"
|
|
454
|
-
version = "0.
|
|
488
|
+
version = "0.5.0"
|
|
455
489
|
dependencies = [
|
|
456
490
|
"mnemo-engine",
|
|
457
491
|
"napi",
|
|
@@ -523,6 +557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
523
557
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
524
558
|
dependencies = [
|
|
525
559
|
"autocfg",
|
|
560
|
+
"libm",
|
|
526
561
|
]
|
|
527
562
|
|
|
528
563
|
[[package]]
|
|
@@ -596,6 +631,32 @@ dependencies = [
|
|
|
596
631
|
"unicode-ident",
|
|
597
632
|
]
|
|
598
633
|
|
|
634
|
+
[[package]]
|
|
635
|
+
name = "proptest"
|
|
636
|
+
version = "1.5.0"
|
|
637
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
638
|
+
checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d"
|
|
639
|
+
dependencies = [
|
|
640
|
+
"bit-set",
|
|
641
|
+
"bit-vec",
|
|
642
|
+
"bitflags",
|
|
643
|
+
"lazy_static",
|
|
644
|
+
"num-traits",
|
|
645
|
+
"rand",
|
|
646
|
+
"rand_chacha",
|
|
647
|
+
"rand_xorshift",
|
|
648
|
+
"regex-syntax",
|
|
649
|
+
"rusty-fork",
|
|
650
|
+
"tempfile",
|
|
651
|
+
"unarray",
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
[[package]]
|
|
655
|
+
name = "quick-error"
|
|
656
|
+
version = "1.2.3"
|
|
657
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
658
|
+
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
|
659
|
+
|
|
599
660
|
[[package]]
|
|
600
661
|
name = "quote"
|
|
601
662
|
version = "1.0.45"
|
|
@@ -607,9 +668,9 @@ dependencies = [
|
|
|
607
668
|
|
|
608
669
|
[[package]]
|
|
609
670
|
name = "rand"
|
|
610
|
-
version = "0.8.
|
|
671
|
+
version = "0.8.6"
|
|
611
672
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
612
|
-
checksum = "
|
|
673
|
+
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
|
613
674
|
dependencies = [
|
|
614
675
|
"libc",
|
|
615
676
|
"rand_chacha",
|
|
@@ -635,6 +696,15 @@ dependencies = [
|
|
|
635
696
|
"getrandom",
|
|
636
697
|
]
|
|
637
698
|
|
|
699
|
+
[[package]]
|
|
700
|
+
name = "rand_xorshift"
|
|
701
|
+
version = "0.3.0"
|
|
702
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
703
|
+
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
|
|
704
|
+
dependencies = [
|
|
705
|
+
"rand_core",
|
|
706
|
+
]
|
|
707
|
+
|
|
638
708
|
[[package]]
|
|
639
709
|
name = "regex"
|
|
640
710
|
version = "1.12.3"
|
|
@@ -726,6 +796,18 @@ version = "1.0.22"
|
|
|
726
796
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
727
797
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
728
798
|
|
|
799
|
+
[[package]]
|
|
800
|
+
name = "rusty-fork"
|
|
801
|
+
version = "0.3.1"
|
|
802
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
803
|
+
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
|
804
|
+
dependencies = [
|
|
805
|
+
"fnv",
|
|
806
|
+
"quick-error",
|
|
807
|
+
"tempfile",
|
|
808
|
+
"wait-timeout",
|
|
809
|
+
]
|
|
810
|
+
|
|
729
811
|
[[package]]
|
|
730
812
|
name = "ryu"
|
|
731
813
|
version = "1.0.23"
|
|
@@ -860,6 +942,12 @@ dependencies = [
|
|
|
860
942
|
"web-time",
|
|
861
943
|
]
|
|
862
944
|
|
|
945
|
+
[[package]]
|
|
946
|
+
name = "unarray"
|
|
947
|
+
version = "0.1.4"
|
|
948
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
949
|
+
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
|
950
|
+
|
|
863
951
|
[[package]]
|
|
864
952
|
name = "unicode-ident"
|
|
865
953
|
version = "1.0.24"
|
|
@@ -894,6 +982,15 @@ version = "0.9.5"
|
|
|
894
982
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
895
983
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
896
984
|
|
|
985
|
+
[[package]]
|
|
986
|
+
name = "wait-timeout"
|
|
987
|
+
version = "0.2.1"
|
|
988
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
989
|
+
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
|
990
|
+
dependencies = [
|
|
991
|
+
"libc",
|
|
992
|
+
]
|
|
993
|
+
|
|
897
994
|
[[package]]
|
|
898
995
|
name = "wasi"
|
|
899
996
|
version = "0.11.1+wasi-snapshot-preview1"
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# below stay `mnemo`, so consumers still write `use mnemo::...` and invoke
|
|
7
7
|
# `mnemo` on the command line after `cargo add mnemo-engine`.
|
|
8
8
|
name = "mnemo-engine"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.5.0"
|
|
10
10
|
edition = "2021"
|
|
11
11
|
rust-version = "1.75"
|
|
12
12
|
description = "Encrypted, single-file, portable agent memory engine with multi-signal recall, sessions, snapshots, and an IVF+PQ index"
|
|
@@ -55,7 +55,14 @@ base64ct = "=1.6.0"
|
|
|
55
55
|
ulid = { version = "=1.1.3", features = ["serde"] }
|
|
56
56
|
thiserror = "=1.0.64"
|
|
57
57
|
zeroize = "=1.8.1"
|
|
58
|
-
|
|
58
|
+
# Pinned `=0.8.6` (patch bump from 0.8.5) to close RUSTSEC-2026-0097,
|
|
59
|
+
# an unsoundness that only triggers under a very narrow config —
|
|
60
|
+
# custom `log::Log` impl reentrantly calling `rand::rng()` on
|
|
61
|
+
# `ThreadRng` mid-reseed — which mnemo does not do. The engine
|
|
62
|
+
# never touches `log`, and no CLI code paths reach `ThreadRng` from
|
|
63
|
+
# a logger. Upgrading anyway is the least-surprising thing to do
|
|
64
|
+
# and satisfies the `advisories` check without an ignore.
|
|
65
|
+
rand = "=0.8.6"
|
|
59
66
|
clap = { version = "=4.4.18", features = ["derive"] }
|
|
60
67
|
# Used by the `mnemo` CLI binary only — `passphrase()` calls it when stdin
|
|
61
68
|
# is a TTY to read the passphrase without echo. The library never touches
|
|
@@ -77,6 +84,19 @@ sha2 = "=0.10.8"
|
|
|
77
84
|
|
|
78
85
|
[dev-dependencies]
|
|
79
86
|
tempfile = "=3.10.1"
|
|
87
|
+
# Property-based testing (Phase 1.3). Pinned exactly; `=1.5.0`'s MSRV
|
|
88
|
+
# is Rust 1.65, well under our 1.75 floor. Kept as a dev-dep only —
|
|
89
|
+
# it does not appear in the shipped library dependency set.
|
|
90
|
+
proptest = "=1.5.0"
|
|
91
|
+
|
|
92
|
+
[features]
|
|
93
|
+
# When enabled, exposes crash-injection hooks used by the Phase 1.3
|
|
94
|
+
# proptest under `mnemo::__failpoints::*`. Always enabled during
|
|
95
|
+
# `cargo test` via `#[cfg(test)]`; the feature is here so an external
|
|
96
|
+
# fault-injection consumer (or a future integration test in a sibling
|
|
97
|
+
# crate) can opt into the same hooks without touching this crate's
|
|
98
|
+
# tests. Not for production use — never enable in a release build.
|
|
99
|
+
failpoints = []
|
|
80
100
|
|
|
81
101
|
[profile.release]
|
|
82
102
|
lto = true
|
|
@@ -41,10 +41,55 @@ mnemo serve --mcp path/to/agent.mnemo
|
|
|
41
41
|
|
|
42
42
|
The passphrase is read from `MNEMO_PASSPHRASE` (never a CLI flag — that
|
|
43
43
|
would leak into shell history and process listings). Tools exposed:
|
|
44
|
-
`about`, `remember`, `recall`, `forget`, `list`, `snapshot_list`, `stats
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
`about`, `remember`, `recall`, `forget`, `list`, `snapshot_list`, `stats`,
|
|
45
|
+
`cache_put`, `cache_get`, `cache_stats`, `cache_put_semantic`,
|
|
46
|
+
`cache_get_semantic`. Every mutation flushes before returning.
|
|
47
|
+
|
|
48
|
+
#### Text-in via `--embed-cmd` (Phase 3)
|
|
49
|
+
|
|
50
|
+
`remember`, `recall`, `cache_put_semantic`, and `cache_get_semantic`
|
|
51
|
+
accept **either** a caller-supplied `vector`/`query` **or** a `text`
|
|
52
|
+
string that the server embeds for them. Enable the text path by passing
|
|
53
|
+
an embedder command at serve start:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
MNEMO_PASSPHRASE=... mnemo serve --mcp agent.mnemo \
|
|
57
|
+
--embed-cmd "python3 mnemo/examples/embedder_server.py" \
|
|
58
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The embedder is a **persistent child process** spawned once at serve
|
|
62
|
+
start. Protocol: it reads one JSON-encoded string per line from stdin
|
|
63
|
+
and writes one JSON array of floats per line to stdout, flushed after
|
|
64
|
+
every reply. See `mnemo/examples/embedder_server.py` for a 10-line
|
|
65
|
+
reference implementation (sentence-transformers + MiniLM, 384-dim).
|
|
66
|
+
|
|
67
|
+
> **Stdout is for replies only.** Any line the embedder prints to
|
|
68
|
+
> stdout outside a reply — startup banners, model-load progress,
|
|
69
|
+
> warnings, "model loaded" messages — is a protocol violation and
|
|
70
|
+
> would shift every subsequent reply by one line. EmbedCmd detects
|
|
71
|
+
> it, kills the child, and surfaces a clear error ("stdout is
|
|
72
|
+
> reserved for replies only; log to stderr instead"); the next call
|
|
73
|
+
> respawns the child. **Log to stderr.** Python: `print("...",
|
|
74
|
+
> file=sys.stderr, flush=True)`, or configure `logging` with
|
|
75
|
+
> `stream=sys.stderr`. Stderr is inherited to the parent and is
|
|
76
|
+
> safe for anything you want to see while the server runs.
|
|
77
|
+
|
|
78
|
+
Per-request timeout defaults to 60 seconds; override with
|
|
79
|
+
`--embed-timeout <secs>` on `serve`, `remember`, and `recall`. Set
|
|
80
|
+
it longer than your slowest cold-start reply (sentence-transformers'
|
|
81
|
+
first `encode()` on CPU can be 3–5s). On timeout — or any other
|
|
82
|
+
protocol error (malformed JSON, wrong dimensionality, zero vector,
|
|
83
|
+
unexpected EOF) — the child is killed and the next call respawns a
|
|
84
|
+
fresh child with the same argv/env/timeout. The guarantee is
|
|
85
|
+
asymmetric and deliberate: **an error is allowed; a mismatched
|
|
86
|
+
vector never is.**
|
|
87
|
+
|
|
88
|
+
The `--embed-name` string is matched against (and persisted on) the
|
|
89
|
+
scaffold manifest's `metadata.embedder = { name, dimensions, normalize }`
|
|
90
|
+
object. Starting a server with a different `--embed-name` on an existing
|
|
91
|
+
file fails loudly — mixing embedders in one file silently corrupts
|
|
92
|
+
recall quality.
|
|
48
93
|
|
|
49
94
|
Claude Desktop config snippet (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
50
95
|
|
|
@@ -53,13 +98,39 @@ Claude Desktop config snippet (`~/Library/Application Support/Claude/claude_desk
|
|
|
53
98
|
"mcpServers": {
|
|
54
99
|
"mnemo": {
|
|
55
100
|
"command": "mnemo",
|
|
56
|
-
"args": [
|
|
101
|
+
"args": [
|
|
102
|
+
"serve", "--mcp", "/absolute/path/to/agent.mnemo",
|
|
103
|
+
"--embed-cmd", "python3 /absolute/path/to/embedder_server.py",
|
|
104
|
+
"--embed-name", "sentence-transformers/all-MiniLM-L6-v2"
|
|
105
|
+
],
|
|
57
106
|
"env": { "MNEMO_PASSPHRASE": "your-passphrase-here" }
|
|
58
107
|
}
|
|
59
108
|
}
|
|
60
109
|
}
|
|
61
110
|
```
|
|
62
111
|
|
|
112
|
+
Omit `--embed-cmd`/`--embed-name` to keep the pre-Phase-3
|
|
113
|
+
embedder-agnostic behavior (`vector`/`query` only).
|
|
114
|
+
|
|
115
|
+
#### CLI text path
|
|
116
|
+
|
|
117
|
+
The same `--embed-cmd` + `--embed-name` pair works on the one-shot
|
|
118
|
+
CLI commands:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
mnemo remember agent.mnemo --text "the user prefers tea" \
|
|
122
|
+
--type semantic --importance 0.8 --agent assistant \
|
|
123
|
+
--embed-cmd "python3 embedder_server.py" \
|
|
124
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2"
|
|
125
|
+
|
|
126
|
+
mnemo recall agent.mnemo --text "what drink?" --top-k 5 \
|
|
127
|
+
--embed-cmd "python3 embedder_server.py" \
|
|
128
|
+
--embed-name "sentence-transformers/all-MiniLM-L6-v2" \
|
|
129
|
+
--no-track
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`--text` is mutually exclusive with `--query`/`--query-file`.
|
|
133
|
+
|
|
63
134
|
The server holds an exclusive write lock on the file for its lifetime
|
|
64
135
|
(see [Concurrency](#concurrency--read-only-opens)), so two MCP servers
|
|
65
136
|
on the same file will conflict — one will fail to start with
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# cargo-deny configuration (Phase 1.4).
|
|
2
|
+
#
|
|
3
|
+
# Runs in CI on every PR via `cargo deny check` in `mnemo/`. Three
|
|
4
|
+
# checks matter here:
|
|
5
|
+
# 1. `advisories` — cross-references the RustSec advisory DB and
|
|
6
|
+
# fails on any unpatched vulnerability affecting a dep in the
|
|
7
|
+
# lockfile.
|
|
8
|
+
# 2. `licenses` — allowlist of OSI-approved licenses compatible
|
|
9
|
+
# with the Apache-2.0 the mnemo crate itself is published under.
|
|
10
|
+
# Anything unlisted requires an explicit `exceptions` entry.
|
|
11
|
+
# 3. `bans` — currently informational (`multiple-versions = "warn"`)
|
|
12
|
+
# to catch transitive duplication without blocking merges while
|
|
13
|
+
# we enumerate the necessary skip set. Follow-up PR will
|
|
14
|
+
# tighten to `deny` once CI has surfaced the real exception
|
|
15
|
+
# list.
|
|
16
|
+
#
|
|
17
|
+
# Run locally with:
|
|
18
|
+
# cd mnemo && cargo deny check
|
|
19
|
+
#
|
|
20
|
+
# For just one check:
|
|
21
|
+
# cargo deny check advisories
|
|
22
|
+
# cargo deny check licenses
|
|
23
|
+
# cargo deny check bans
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------
|
|
26
|
+
# Graph configuration — what crates the checks apply to.
|
|
27
|
+
# ---------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
[graph]
|
|
30
|
+
# Empty `targets` means "check all crates for all target triples the
|
|
31
|
+
# lockfile references." Since mnemo compiles on Linux/macOS/Windows
|
|
32
|
+
# x86_64+aarch64, restricting to a single target would hide license /
|
|
33
|
+
# advisory hits on platform-specific transitives (e.g. `windows-sys`
|
|
34
|
+
# on Windows only).
|
|
35
|
+
targets = []
|
|
36
|
+
all-features = false
|
|
37
|
+
no-default-features = false
|
|
38
|
+
|
|
39
|
+
[output]
|
|
40
|
+
# Group violations by feature depth; keeps the diagnostic readable
|
|
41
|
+
# on the multi-page transitive graph typical of a Rust crate.
|
|
42
|
+
feature-depth = 1
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------
|
|
45
|
+
# Advisory DB — RustSec vulnerability + yanked-crate check.
|
|
46
|
+
# ---------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
[advisories]
|
|
49
|
+
version = 2
|
|
50
|
+
db-path = "~/.cargo/advisory-db"
|
|
51
|
+
db-urls = ["https://github.com/rustsec/advisory-db"]
|
|
52
|
+
# `yanked` fires when Cargo.lock pins a version the author later
|
|
53
|
+
# yanked from crates.io. `warn` first so we notice; upgrade to
|
|
54
|
+
# `deny` once we've picked up a clean baseline.
|
|
55
|
+
yanked = "warn"
|
|
56
|
+
# Time-boxed / justified exceptions to the advisory check. Every
|
|
57
|
+
# entry must carry a `reason` — a future worker reading this file
|
|
58
|
+
# should be able to tell why the ignore was granted without leaving
|
|
59
|
+
# the config.
|
|
60
|
+
ignore = [
|
|
61
|
+
# `paste 1.0.15` — dtolnay archived the repo (RUSTSEC-2024-0436,
|
|
62
|
+
# "unmaintained"). NOT a vulnerability. It is pulled transitively
|
|
63
|
+
# by `rmp` / `rmp-serde` (our MessagePack layer) which have no
|
|
64
|
+
# migration path today. The advisory itself suggests `pastey` as
|
|
65
|
+
# a drop-in fork; that migration lives in `rmp`, not here. Revisit
|
|
66
|
+
# when `rmp-serde` ships a release that swaps to `pastey`.
|
|
67
|
+
{ id = "RUSTSEC-2024-0436", reason = "paste (unmaintained) is a transitive dep of rmp-serde; no upstream migration path yet — track via rmp-serde issue tracker" },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------
|
|
71
|
+
# Licenses — SPDX allowlist. Anything not listed → build fails.
|
|
72
|
+
# ---------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
[licenses]
|
|
75
|
+
version = 2
|
|
76
|
+
# Kept minimal — the mnemo dep graph in practice only ships crates
|
|
77
|
+
# under Apache-2.0, MIT, BSD-3-Clause, and Unicode-3.0 (the newer
|
|
78
|
+
# SPDX variant `unicode-ident` uses). Apache-2.0 WITH LLVM-exception
|
|
79
|
+
# covers the `rustix` family. Adding an unused allow is not a security
|
|
80
|
+
# hole but it's noise — cargo-deny prints an "unmatched license
|
|
81
|
+
# allowance" warning for each, and drift in the allowlist becomes
|
|
82
|
+
# hard to notice. Extend this list only when a new dep actually
|
|
83
|
+
# ships under something not already here.
|
|
84
|
+
allow = [
|
|
85
|
+
"Apache-2.0",
|
|
86
|
+
"Apache-2.0 WITH LLVM-exception",
|
|
87
|
+
"MIT",
|
|
88
|
+
"BSD-2-Clause",
|
|
89
|
+
"BSD-3-Clause",
|
|
90
|
+
"Unicode-3.0",
|
|
91
|
+
]
|
|
92
|
+
# Threshold for guessing a license from a LICENSE file's text when
|
|
93
|
+
# the crate omits SPDX metadata. Default 0.8 is conservative — lower
|
|
94
|
+
# it only if a specific crate needs a `[[licenses.clarify]]` entry.
|
|
95
|
+
confidence-threshold = 0.8
|
|
96
|
+
# Per-crate license overrides go here if a crate ships an unusual
|
|
97
|
+
# LICENSE mix cargo-deny can't parse. Empty at PR 9 landing;
|
|
98
|
+
# expand as CI surfaces cases.
|
|
99
|
+
exceptions = []
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------
|
|
102
|
+
# Bans — duplicate versions, wildcard deps, per-crate allow/deny.
|
|
103
|
+
# ---------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
[bans]
|
|
106
|
+
# `warn` for now: transitive graphs commonly duplicate `syn 1/2`,
|
|
107
|
+
# `regex-syntax 0.6/0.8`, `bitflags 1/2`, `windows-sys 0.4x/0.5x`
|
|
108
|
+
# during migrations. First green CI run will list the actual
|
|
109
|
+
# duplicates; follow-up PR flips to `deny` with the enumerated
|
|
110
|
+
# `skip` entries.
|
|
111
|
+
multiple-versions = "warn"
|
|
112
|
+
wildcards = "allow"
|
|
113
|
+
highlight = "all"
|
|
114
|
+
workspace-default-features = "allow"
|
|
115
|
+
external-default-features = "allow"
|
|
116
|
+
allow = []
|
|
117
|
+
deny = []
|
|
118
|
+
skip = []
|
|
119
|
+
skip-tree = []
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------
|
|
122
|
+
# Sources — where crates may come from.
|
|
123
|
+
# ---------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
[sources]
|
|
126
|
+
unknown-registry = "warn"
|
|
127
|
+
unknown-git = "warn"
|
|
128
|
+
# Only crates.io. No git deps in mnemo (all pinned to `=x.y.z` per
|
|
129
|
+
# AGENTS.md), so this stays tight.
|
|
130
|
+
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|
131
|
+
allow-git = []
|