Skip to main content

Mnemo

Struct Mnemo 

Source
pub struct Mnemo { /* private fields */ }
Expand description

An encrypted, single-file agent memory database.

Implementations§

Source§

impl Mnemo

Source

pub fn create( path: impl Into<PathBuf>, passphrase: &str, config: MnemoConfig, ) -> Result<Mnemo>

Create a brand-new encrypted database at path.

Source

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.

Source

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:

  1. 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.
  2. 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.

Source

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.

Source

pub fn dimensions(&self) -> usize

Embedding dimensionality this database expects.

Source

pub fn len(&self) -> usize

Number of live (non-deleted) memories.

Source

pub fn is_empty(&self) -> bool

True if the database holds no live memories.

Source

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:

  1. Dimension check. The embedder’s identity().dimensions must equal Mnemo::dimensions (the width this database was created with). Mismatch returns MnemoError::DimensionMismatch.
  2. Manifest identity check. If the scaffold / onboarding manifest already records an embedder — the canonical shape is metadata.embedder = { name, dimensions, normalize }, with the legacy flat embedder_name / embedder_dimensions / embedder_normalize keys accepted as an alias — the new embedder’s identity must match exactly. A different name — even at the same dimensions — returns MnemoError::Invalid with [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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn get(&mut self, id: &Ulid) -> Result<Memory>

Fetch a memory by ID.

Source

pub fn delete(&mut self, id: &Ulid) -> Result<()>

Soft-delete a memory (tombstoned; space reclaimed by compact).

Source

pub fn memories(&mut self) -> Result<Vec<Memory>>

Return every live memory (used by tooling and compaction).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn build_index_with(&mut self, cfg: IndexConfig) -> Result<IndexInfo>

Build the ANN index with explicit tuning.

Source

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.

Source

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).

Source

pub fn has_index(&self) -> bool

Whether an ANN index is currently loaded.

Source

pub fn flush(&mut self) -> Result<()>

Persist all pending changes as one write-ahead-logged transaction.

Sequence:

  1. Prepare. Serialize the control plane (catalog, ANN index), grow the WAL if needed, and lease counter and page slots — bump header.write_counter and header.next_page by upper bounds on this transaction’s consumption and persist that leased header to disk before any encrypted page is written.
  2. 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 a write_counter past anything this step can produce.
  3. Control plane. Seal the new catalog / ANN index / snapshot manifest into fresh home page runs.
  4. Commit. Log all of step 3’s frames plus the final header frame into the WAL and fsync — the durability point.
  5. 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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

pub fn stats(&mut self) -> Result<Stats>

Summary statistics for the open database.

Source

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.

Auto Trait Implementations§

§

impl !RefUnwindSafe for Mnemo

§

impl !UnwindSafe for Mnemo

§

impl Forget for Mnemo

§

impl Freeze for Mnemo

§

impl Send for Mnemo

§

impl Sync for Mnemo

§

impl Unpin for Mnemo

§

impl UnsafeUnpin for Mnemo

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V