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.
Files changed (40) hide show
  1. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/PKG-INFO +71 -1
  2. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/README.md +70 -0
  3. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/Cargo.lock +101 -4
  4. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/Cargo.toml +22 -2
  5. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/README.md +76 -5
  6. mnemo_engine-0.5.0/mnemo/deny.toml +131 -0
  7. mnemo_engine-0.5.0/mnemo/examples/embedder_server.py +51 -0
  8. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/bin/mnemo.rs +273 -15
  9. mnemo_engine-0.5.0/mnemo/src/embed.rs +280 -0
  10. mnemo_engine-0.5.0/mnemo/src/embed_cmd.rs +572 -0
  11. mnemo_engine-0.5.0/mnemo/src/lib.rs +162 -0
  12. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/mcp.rs +284 -82
  13. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/memory.rs +13 -1
  14. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/pager.rs +72 -0
  15. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/store.rs +159 -0
  16. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/wal.rs +25 -4
  17. mnemo_engine-0.5.0/mnemo/tests/cli_smoke.rs +1336 -0
  18. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/tests/integration.rs +370 -10
  19. mnemo_engine-0.5.0/mnemo/tests/proptest_store.proptest-regressions +8 -0
  20. mnemo_engine-0.5.0/mnemo/tests/proptest_store.rs +526 -0
  21. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/Cargo.lock +4 -4
  22. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/Cargo.toml +1 -1
  23. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/README.md +70 -0
  24. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo-python/src/lib.rs +416 -10
  25. mnemo_engine-0.5.0/mnemo-python/test_mnemo.py +575 -0
  26. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/pyproject.toml +1 -1
  27. mnemo_engine-0.4.0/mnemo/src/lib.rs +0 -80
  28. mnemo_engine-0.4.0/mnemo/tests/cli_smoke.rs +0 -553
  29. mnemo_engine-0.4.0/mnemo-python/test_mnemo.py +0 -240
  30. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/.gitignore +0 -0
  31. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/examples/quickstart.rs +0 -0
  32. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/examples/session.rs +0 -0
  33. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/cache.rs +0 -0
  34. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/crypto.rs +0 -0
  35. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/error.rs +0 -0
  36. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/format.rs +0 -0
  37. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/index.rs +0 -0
  38. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/result_cache.rs +0 -0
  39. {mnemo_engine-0.4.0 → mnemo_engine-0.5.0}/mnemo/src/session.rs +0 -0
  40. {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.4.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.4.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.4.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.5"
671
+ version = "0.8.6"
611
672
  source = "registry+https://github.com/rust-lang/crates.io-index"
612
- checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
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.4.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
- rand = "=0.8.5"
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
- Every mutation flushes before returning. `remember` and `recall` are
46
- embedder-agnostic — the caller supplies vectors; text-only variants
47
- land with Phase 3 of the level-up plan.
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": ["serve", "--mcp", "/absolute/path/to/agent.mnemo"],
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 = []