pub struct Mnemo { /* private fields */ }Expand description
An encrypted, single-file agent memory database.
Implementations§
Source§impl Mnemo
impl Mnemo
Sourcepub fn create(
path: impl Into<PathBuf>,
passphrase: &str,
config: MnemoConfig,
) -> Result<Mnemo>
pub fn create( path: impl Into<PathBuf>, passphrase: &str, config: MnemoConfig, ) -> Result<Mnemo>
Create a brand-new encrypted database at path.
Sourcepub fn open(path: impl Into<PathBuf>, passphrase: &str) -> Result<Mnemo>
pub fn open(path: impl Into<PathBuf>, passphrase: &str) -> Result<Mnemo>
Open an existing database. A wrong passphrase fails cleanly with
MnemoError::WrongPassphrase. Takes an OS exclusive lock for
the lifetime of the returned handle; a second concurrent open
(from any process) returns MnemoError::Locked.
For a caller that only needs to read (and can tolerate the
pending-recovery / pending-migration cases documented on
Mnemo::open_read_only), prefer that entry point — it takes a
shared lock and coexists with other readers.
Sourcepub fn open_read_only(
path: impl Into<PathBuf>,
passphrase: &str,
) -> Result<Mnemo>
pub fn open_read_only( path: impl Into<PathBuf>, passphrase: &str, ) -> Result<Mnemo>
Open an existing database in read-only mode. Takes an OS shared
lock; multiple read-only handles can coexist, but any attempt to
open the same file read-write elsewhere returns
MnemoError::Locked. All mutating methods on the returned handle
return MnemoError::ReadOnly, including recall with
track_access(true).
Two conditions block a read-only open and return
MnemoError::NeedsWriteOpen:
- The file has a committed-but-uncheckpointed WAL transaction that would ordinarily be replayed on open. A read-only handle cannot write the recovered pages back.
- The on-disk format version is older than the current
format::VERSION, requiring an on-disk migration.
In either case, open the file read-write once (which recovers / migrates in place), close the handle cleanly, and reopen read-only. This is documented in the README’s Durability section.
Sourcepub fn set_max_snapshots(&mut self, max: usize)
pub fn set_max_snapshots(&mut self, max: usize)
Override the manifest snapshot cap on this open handle. 0 disables
the cap; any positive value keeps the most-recent max snapshots and
drops the rest on the next flush. See MnemoConfig::max_snapshots.
Sourcepub fn dimensions(&self) -> usize
pub fn dimensions(&self) -> usize
Embedding dimensionality this database expects.
Sourcepub fn set_embedder<E: Embedder + 'static>(&mut self, embedder: E) -> Result<()>
pub fn set_embedder<E: Embedder + 'static>(&mut self, embedder: E) -> Result<()>
Register a text embedder for this handle. Enables
Mnemo::remember_text and the text-side RecallRequest
convenience path. Does two checks before storing the embedder:
- Dimension check. The embedder’s
identity().dimensionsmust equalMnemo::dimensions(the width this database was created with). Mismatch returnsMnemoError::DimensionMismatch. - Manifest identity check. If the scaffold / onboarding
manifest already records an embedder — the canonical shape
is
metadata.embedder = { name, dimensions, normalize }, with the legacy flatembedder_name/embedder_dimensions/embedder_normalizekeys accepted as an alias — the new embedder’s identity must match exactly. A different name — even at the same dimensions — returnsMnemoError::Invalidwith [EmbedderIdentity::MISMATCH_MSG] in the message. If the manifest has no embedder metadata yet, this call upserts the nested object into the first onboarding/manifest memory (write + flush so a subsequent reopen sees the identity).
Read-only handles. set_embedder on an open_read_only
handle performs both checks and adopts the embedder in memory;
it never writes. Readers (CLI --text, agent-side recall_text)
can therefore use the text path next to a writer that holds
the exclusive lock.
On both failure paths the handle’s embedder slot is left
unchanged. On success the embedder is wrapped in an Arc<dyn Embedder> and consulted by every text-side call made through
this handle. The slot is per-handle and not persisted — a
fresh open must re-register (same identity, so the manifest
check round-trips cleanly).
Sourcepub fn embedder(&self) -> Option<&dyn Embedder>
pub fn embedder(&self) -> Option<&dyn Embedder>
Borrow the currently-registered embedder, if any. Returns
None when no embedder has been set via
Mnemo::set_embedder. Bindings use this to drive their own
text-side convenience methods without duplicating the identity
check.
Sourcepub fn embedder_arc(&self) -> Option<Arc<dyn Embedder>>
pub fn embedder_arc(&self) -> Option<Arc<dyn Embedder>>
Shared handle to the registered embedder, cloned so the caller
can drop the borrow on this Mnemo before invoking embed().
FIX-1 / PY-1 (R-001.11): the Python binding needs this so an
embedder callback that re-enters Mnemo (e.g. a Python
embedder that calls db.dimensions() or db.cache_get(...))
doesn’t hit PyO3’s “Already borrowed” — we clone the Arc under
a short &self borrow, release that borrow, then invoke
embed() with no &mut Mnemo held.
Sourcepub fn remember_text(
&mut self,
content: &str,
memory_type: MemoryType,
) -> Result<Ulid>
pub fn remember_text( &mut self, content: &str, memory_type: MemoryType, ) -> Result<Ulid>
Embed content with the registered embedder and remember the
result. Returns the new memory’s ULID. Convenience path for
callers who have a string, want it stored, and don’t need any
of the per-memory options Memory exposes (agent, importance,
metadata, ttl, scope) — build a Memory by hand and call
Mnemo::remember for those.
Fails with MnemoError::Invalid if no embedder has been
registered on this handle (call Mnemo::set_embedder first);
with MnemoError::DimensionMismatch if the embedder returned
the wrong width for this database.
Sourcepub fn remember(&mut self, memory: Memory) -> Result<Ulid>
pub fn remember(&mut self, memory: Memory) -> Result<Ulid>
Store a memory. Returns its ULID. If the memory’s ID is nil a fresh one is assigned; an existing ID overwrites in place.
Sourcepub fn delete(&mut self, id: &Ulid) -> Result<()>
pub fn delete(&mut self, id: &Ulid) -> Result<()>
Soft-delete a memory (tombstoned; space reclaimed by compact).
Sourcepub fn memories(&mut self) -> Result<Vec<Memory>>
pub fn memories(&mut self) -> Result<Vec<Memory>>
Return every live memory (used by tooling and compaction).
Sourcepub fn about(&mut self) -> Result<Vec<Memory>>
pub fn about(&mut self) -> Result<Vec<Memory>>
Return the database’s self-describing onboarding memories — the
ones tagged metadata.area = "onboarding". This is the engine-level
surface for the single-file philosophy: an agent who receives a
.mnemo file (and its passphrase) can call this to learn what the
file is, which embedder it expects, the recommended agent id, and any
other conventions the file’s author chose to record — all without
needing any external documentation.
Ordering: the canonical manifest (tag metadata.topic = "manifest")
always comes first; everything else follows in importance descending,
then created_at ascending for deterministic results.
Sourcepub fn search(
&mut self,
query: &[f32],
top_k: usize,
metric: Metric,
) -> Result<Vec<(Memory, f32)>>
pub fn search( &mut self, query: &[f32], top_k: usize, metric: Metric, ) -> Result<Vec<(Memory, f32)>>
Phase-1 brute-force search: rank live memories by raw similarity only. Read-only — does not update access statistics.
Sourcepub fn recall(&mut self, req: &RecallRequest) -> Result<Vec<RecallResult>>
pub fn recall(&mut self, req: &RecallRequest) -> Result<Vec<RecallResult>>
Phase-5 multi-signal recall: rank by
α·similarity + β·recency + γ·importance + δ·ln(freq), with type and
agent filtering. Updates accessed_at / access_count on the
returned memories (persisted on the next flush).
When an ANN index has been built (Mnemo::build_index), recall runs
the tiered IVF→PQ→rerank pipeline: it scores only the similarity-nearest
n_rerank candidates rather than every memory. Without an index it
scores every live memory exactly.
Sourcepub fn build_index(&mut self) -> Result<IndexInfo>
pub fn build_index(&mut self) -> Result<IndexInfo>
Build an IVF+PQ approximate-nearest-neighbour index over every live
memory, using default tuning. After this, Mnemo::recall runs the
tiered pipeline instead of an exact scan. Persisted on the next
flush. Returns a snapshot of the index shape.
Sourcepub fn build_index_with(&mut self, cfg: IndexConfig) -> Result<IndexInfo>
pub fn build_index_with(&mut self, cfg: IndexConfig) -> Result<IndexInfo>
Build the ANN index with explicit tuning.
Sourcepub fn rebuild_index(&mut self) -> Result<IndexInfo>
pub fn rebuild_index(&mut self) -> Result<IndexInfo>
Rebuild the ANN index from scratch — re-clusters centroids and retrains the PQ codebook, undoing the cluster drift that accumulates as memories are inserted against fixed centroids.
Sourcepub fn drop_index(&mut self)
pub fn drop_index(&mut self)
Drop the ANN index; recall reverts to exact scans. Persisted on flush.
No-op on a read-only handle (signature is infallible — callers of
build_index / flush on a read-only handle already get a typed
ReadOnly error, which is the enforceable boundary).
Sourcepub fn flush(&mut self) -> Result<()>
pub fn flush(&mut self) -> Result<()>
Persist all pending changes as one write-ahead-logged transaction.
Sequence:
- Prepare. Serialize the control plane (catalog, ANN index),
grow the WAL if needed, and lease counter and page slots —
bump
header.write_counterandheader.next_pageby upper bounds on this transaction’s consumption and persist that leased header to disk before any encrypted page is written. - Data pages. [
Pager::flush] writes dirty record pages copy-on-write under fresh nonces and fsyncs them. The orphan-page nonce-reuse window (see [Phase 1.1 of the improvement plan]) is closed because the leased header on disk already records awrite_counterpast anything this step can produce. - Control plane. Seal the new catalog / ANN index / snapshot manifest into fresh home page runs.
- Commit. Log all of step 3’s frames plus the final header frame into the WAL and fsync — the durability point.
- Checkpoint. Fold the WAL into the home pages.
A crash before step 4 leaves the previous state intact. A crash
after step 4 is repaired by Mnemo::open replaying the WAL.
Safe to call repeatedly.
Sourcepub fn close(&mut self) -> Result<()>
pub fn close(&mut self) -> Result<()>
Flush and close. Equivalent to flush() on a read-write handle; on
a read-only handle it’s a no-op (no dirty state to persist). The
file is released on drop, which also drops the OS advisory lock.
Sourcepub fn set_cache_capacity(&mut self, pages: usize)
pub fn set_cache_capacity(&mut self, pages: usize)
Bound the in-memory page cache to pages decrypted pages.
The cache holds decrypted page payloads to speed repeated reads. By
default it is capped at 8192 pages (~64 MiB); lower the cap to trade
hit rate for a smaller footprint, or raise it for a hotter cache. The
cap governs clean pages — pages with un-flushed writes are always
retained until Mnemo::flush, regardless of the cap.
Sourcepub fn page_cache_stats(&self) -> (usize, usize)
pub fn page_cache_stats(&self) -> (usize, usize)
Page-cache occupancy: (pages_cached, capacity). Renamed from
cache_stats in v0.4.0: the result-cache API (Phase 10.1) took
that name for its per-namespace CacheStats summary, and the
page-cache pair is the older/less-user-facing of the two, so it
gets the more-specific name.
Sourcepub fn cache_put(
&mut self,
namespace: &str,
key: &str,
value: &[u8],
opts: CachePutOpts,
) -> Result<()>
pub fn cache_put( &mut self, namespace: &str, key: &str, value: &[u8], opts: CachePutOpts, ) -> Result<()>
Insert or overwrite a cache entry at (namespace, key).
The engine SHA-256-hashes key — callers pass the raw input
string. Persists to disk on the next flush() under the current
flush policy (see Mnemo::set_cache_flush_policy); in
CacheFlushPolicy::Batched the engine may call flush()
internally when a threshold trips.
On overflow of the namespace’s CacheBudget (default 10,000
entries / 64 MiB) the LRU entry is tombstoned before the new
insert. Tombstones are reclaimed by
Mnemo::compact_file, not the runtime flush.
Returns MnemoError::ReadOnly on a read-only handle.
Sourcepub fn cache_get(
&mut self,
namespace: &str,
key: &str,
) -> Result<Option<CachedValue>>
pub fn cache_get( &mut self, namespace: &str, key: &str, ) -> Result<Option<CachedValue>>
Look up (namespace, key). Returns None on miss (either the
key was never inserted or the entry is TTL-expired). On hit
updates catalog-side access stats (mirrors the recall v5-trick);
on a read-only handle the stats update is silently skipped.
A miss is recorded as a miss in this handle’s counters even if the key was TTL-expired — from the caller’s perspective the two are indistinguishable.
Sourcepub fn cache_delete(&mut self, namespace: &str, key: &str) -> Result<()>
pub fn cache_delete(&mut self, namespace: &str, key: &str) -> Result<()>
Tombstone the entry at (namespace, key). Silently succeeds if
no such entry exists. Space is reclaimed by compact_file.
Sourcepub fn cache_purge(
&mut self,
namespace: Option<&str>,
expired_only: bool,
) -> Result<usize>
pub fn cache_purge( &mut self, namespace: Option<&str>, expired_only: bool, ) -> Result<usize>
Purge cache entries. If namespace is Some, restrict to that
namespace; otherwise purge across the whole cache. If
expired_only is true, only TTL-expired entries are removed;
otherwise every live entry in the target scope is tombstoned.
Returns the number of entries newly tombstoned.
Sourcepub fn cache_stats(&self, namespace: Option<&str>) -> CacheStats
pub fn cache_stats(&self, namespace: Option<&str>) -> CacheStats
Statistics for namespace (or the whole cache if None).
hits/misses/evictions are per-handle in-memory counters
that reset on reopen; entries/bytes reflect the durable
on-disk state (live entries only, tombstones excluded).
Sourcepub fn set_cache_budget(&mut self, namespace: &str, budget: CacheBudget)
pub fn set_cache_budget(&mut self, namespace: &str, budget: CacheBudget)
Set (or update) the eviction budget for namespace. Applies to
subsequent cache_put calls; does not retroactively evict
entries that already sit under a looser cap.
Sourcepub fn set_cache_flush_policy(&mut self, policy: CacheFlushPolicy)
pub fn set_cache_flush_policy(&mut self, policy: CacheFlushPolicy)
Set the per-handle cache flush policy. Defaults to
CacheFlushPolicy::Strict. Memory writes are never batched;
this affects only cache mutations.
Sourcepub fn cache_put_semantic(
&mut self,
namespace: &str,
key: &str,
vector: Vec<f32>,
value: &[u8],
opts: SemanticCachePutOpts,
) -> Result<()>
pub fn cache_put_semantic( &mut self, namespace: &str, key: &str, vector: Vec<f32>, value: &[u8], opts: SemanticCachePutOpts, ) -> Result<()>
Insert or overwrite a semantic cache entry — same as
Mnemo::cache_put but with a required embedding vector and
model identifier. The vector is stored on the directory entry
so Mnemo::cache_get_semantic can score without decrypting
record bodies.
Vector dimensionality is validated against the database’s
configured dimensions — a mismatch returns
MnemoError::DimensionMismatch, the same signal remember
gives for the memory catalog.
The model string is matched exactly on lookup — a
different model’s cache is invisible to a given query. Pick
canonical identifiers ("text-embedding-3-small",
"bge-large-en-v1.5", …) and stick to them.
Sourcepub fn cache_get_semantic(
&mut self,
namespace: &str,
query_vector: &[f32],
threshold: f32,
model: &str,
) -> Result<Option<(CachedValue, f32)>>
pub fn cache_get_semantic( &mut self, namespace: &str, query_vector: &[f32], threshold: f32, model: &str, ) -> Result<Option<(CachedValue, f32)>>
Semantic recall over the cache: score every live entry in
namespace whose model matches against query_vector using
cosine similarity; return the top-1 hit and its similarity iff
sim >= threshold. None otherwise.
Uses crate::result_cache::DEFAULT_SEMANTIC_THRESHOLD if you
want a sensible default — pass it explicitly (0.97 today).
Exact-key entries (those without a vector) are transparently skipped, as are entries whose model doesn’t match — a different-model hit would be a semantic bug, not a win. TTL-expired entries are also skipped.
Access-stat updates on hit follow the same policy as
Mnemo::cache_get: bumped on read-write handles, silently
skipped on read-only. Hits and misses are recorded in the
per-handle counters just like exact-key lookups.
Sourcepub fn session(&mut self, agent_id: impl Into<String>) -> Session<'_>
pub fn session(&mut self, agent_id: impl Into<String>) -> Session<'_>
Begin a conversation Session for agent_id.
The session borrows the database for its lifetime, records turns as working memory, and consolidates them into episodic memory when closed.
Sourcepub fn snapshots(&self) -> Vec<SnapshotInfo>
pub fn snapshots(&self) -> Vec<SnapshotInfo>
Every committed transaction, oldest first — the restore points
available to Mnemo::restore_to and Mnemo::restore_to_time.
Each flush appends one snapshot. Because the storage engine is
append-only, the pages a past flush wrote are still on disk, so any
listed snapshot can be reinstated exactly. The history reaches back to
the last Mnemo::compact_file, which reclaims space by collapsing
it.
Sourcepub fn restore_to(&mut self, txn_id: u64) -> Result<SnapshotInfo>
pub fn restore_to(&mut self, txn_id: u64) -> Result<SnapshotInfo>
Restore the database to the snapshot produced by transaction txn_id.
The restore is itself a new committed transaction (and a new snapshot), so it is crash-safe and reversible — restoring forward to a later snapshot afterwards works just as well.
Sourcepub fn restore_to_time(&mut self, unix_secs: i64) -> Result<SnapshotInfo>
pub fn restore_to_time(&mut self, unix_secs: i64) -> Result<SnapshotInfo>
Restore the database to the latest snapshot committed at or before
unix_secs. Returns MnemoError::NotFound if no snapshot is that
old. Like Mnemo::restore_to, the restore is a new transaction.
Sourcepub fn kdf_params(&self) -> KdfParams
pub fn kdf_params(&self) -> KdfParams
Return the KDF (Argon2id) parameters this file currently uses to
wrap its DEK. Needed by callers that want to [rekey] while
keeping the file’s original protection level (e.g. the Python
binding’s rekey(fast=False), whose docstring promises exactly
that). FIX-1 R-001.5.
Sourcepub fn export_encrypted(&mut self, dest: impl AsRef<Path>) -> Result<()>
pub fn export_encrypted(&mut self, dest: impl AsRef<Path>) -> Result<()>
Flush any pending writes, then stream the (encrypted) file to
dest atomically: writes to a sibling temp file in dest’s
parent directory, sync_alls it, renames onto dest, and
fsyncs the parent directory on Unix so a crash never leaves a
partial export.
Refuses when dest resolves to the file this handle has open
— that case would otherwise File::create-truncate the live
database to 0 bytes (fs::copy on Unix reads after the
O_TRUNC, so a naive export-to-self destroys the source).
Unix: compares (dev, ino) of both paths via MetadataExt,
catching symlinks and hard links. Windows: compares
canonicalized paths; a rename onto an open file also fails on
Windows because of the mandatory lock the handle holds, so the
rename step is a second line of defense.
Reads the bytes from the already-open file handle, not by
fs::copy. On Windows that matters: LockFileEx locks are
mandatory, so a second handle opened by fs::copy would fail
while this writer still holds the lock.
FIX-1 R-001.5 + R-002.2 part 3.
Sourcepub fn rekey(&mut self, new_passphrase: &str, kdf: KdfParams) -> Result<()>
pub fn rekey(&mut self, new_passphrase: &str, kdf: KdfParams) -> Result<()>
Change the passphrase. Cheap: re-derives the KEK and re-wraps the DEK; the encrypted pages are never rewritten.
Sourcepub fn verify(&mut self) -> Result<usize>
pub fn verify(&mut self) -> Result<usize>
Decrypt and validate every live record. Returns the count verified
across memories and cache entries (FIX-1 R-001.1: now that the
cache survives compaction, verify() has to walk it too — the
pre-FIX-1 implementation only checked self.catalog, so a
corrupted cache record went unnoticed).
Sourcepub fn compact_file(path: &str, passphrase: &str) -> Result<CompactReport>
pub fn compact_file(path: &str, passphrase: &str) -> Result<CompactReport>
Rewrite the file, dropping tombstoned and expired memories and reclaiming stale pages left by updates. Done out-of-place via a temp file and an atomic rename.
FIX-1 R-001.1 + R-002.2 part 2: the result cache now survives
compaction. Every live, non-expired directory entry is copied
into the new file verbatim — created_at, ttl_secs,
cost_hint_ms, key_preview, size_bytes, content_type,
vector and model come from the record body; accessed_at
and access_count come from the directory (the record body
only holds the insert-time snapshot — the directory is the
source of truth for the live counts, as of v5’s recall trick).
Tombstoned and TTL-expired entries are dropped. Pre-FIX-1
compaction kept only the memory catalog and silently emptied
the cache; the Planner’s interim README caveat (c417d2e) called
that out — this PR removes the surprise.