Skip to main content

mnemo_engine/
lib.rs

1//! # Mnemo
2//!
3//! Mnemo is an **encrypted, single-file, portable agent-memory engine**.
4//!
5//! A whole memory store — vectors, content, metadata, and the multi-signal
6//! recall machinery an agent needs — lives in one file you can copy, back up,
7//! or hand to another process. The file is encrypted at rest with a two-tier
8//! key hierarchy: an Argon2id key-encryption key (KEK) derived from a
9//! passphrase wraps a random data-encryption key (DEK), and the DEK encrypts
10//! every page with AES-256-GCM.
11//!
12//! ## Quick start
13//!
14//! ```no_run
15//! use mnemo_engine::{Mnemo, MnemoConfig, Memory, MemoryType, RecallRequest};
16//!
17//! # fn main() -> mnemo_engine::Result<()> {
18//! let cfg = MnemoConfig { dimensions: 3, ..Default::default() };
19//! let mut db = Mnemo::create("agent.mnemo", "correct horse battery", cfg)?;
20//!
21//! db.remember(
22//!     Memory::new("the user prefers dark mode", MemoryType::Semantic, vec![0.1, 0.2, 0.9])
23//!         .with_agent("assistant-1")
24//!         .with_importance(0.8),
25//! )?;
26//! db.flush()?;
27//!
28//! let hits = db.recall(&RecallRequest::new(vec![0.1, 0.2, 0.9]).top_k(5))?;
29//! for h in hits {
30//!     println!("{:.3}  {}", h.score, h.memory.content);
31//! }
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! ## What is and is not built
37//!
38//! This crate implements a real, tested core: the encrypted single-file
39//! storage engine, the crypto layer, the agent-memory model, multi-signal
40//! recall, an IVF+PQ approximate-nearest-neighbour index, a write-ahead log,
41//! snapshot-based point-in-time recovery, a bounded LRU page cache, and the
42//! [`Session`] conversation wrapper. Exact brute-force search remains
43//! available as the ground-truth baseline; a built index makes
44//! [`Mnemo::recall`] sub-linear. Each [`Mnemo::flush`] is one atomic,
45//! WAL-committed transaction and a restorable snapshot — [`Mnemo::restore_to`]
46//! rewinds the database to any past transaction. Python and TypeScript
47//! language bindings are the one documented roadmap item — see the README.
48
49#![forbid(unsafe_code)]
50#![warn(missing_docs)]
51
52mod cache;
53mod crypto;
54mod embed;
55mod embed_cmd;
56mod error;
57mod format;
58mod index;
59pub mod mcp;
60mod memory;
61mod pager;
62mod result_cache;
63mod session;
64mod store;
65mod transcript;
66mod wal;
67
68pub use crypto::KdfParams;
69pub use embed::{Embedder, EmbedderIdentity};
70pub use embed_cmd::{EmbedCmd, DEFAULT_EMBED_TIMEOUT};
71pub use error::{MnemoError, Result};
72pub use index::{IndexConfig, IndexInfo};
73pub use memory::{
74    status as essence_status, CommitBatch, CommitReport, EssenceOp, EssenceOpKind, EssenceScope,
75    EssenceSection, ExportedTurn, Filter, FilterTerm, IngestReport, KeyScope, KeyedMemory, Memory,
76    MemoryPatch, MemoryType, Metric, Scope, ScoreWeights, ThreadTurn, TurnPolicy, WriteOutcome,
77    CONTINUATION_TIMELINE_ENTRIES, ESSENCE_DEFAULT_RENDER_BYTES, ESSENCE_SECTION_MAX_BYTES,
78    ESSENCE_TIMELINE_MAX_BYTES, ESSENCE_TRANSITIONS, FINDING_TRANSITIONS, MNEMO_META_CONTINUES,
79    MNEMO_META_INVALID_AT, MNEMO_META_KEY, MNEMO_META_KIND, MNEMO_META_KIND_ESSENCE,
80    MNEMO_META_KIND_THREAD_LINK, MNEMO_META_KIND_TURN, MNEMO_META_LENGTH, MNEMO_META_NS,
81    MNEMO_META_PART, MNEMO_META_REFS, MNEMO_META_ROLE, MNEMO_META_SECTION, MNEMO_META_SEQ,
82    MNEMO_META_SHA256, MNEMO_META_STATUS, MNEMO_META_STATUS_HISTORY, MNEMO_META_SUPERSEDED_BY,
83    MNEMO_META_SUPERSEDES, MNEMO_META_THREAD, MNEMO_META_TURN_ID, MNEMO_META_UPDATED_AT,
84    TASK_TRANSITIONS, THREAD_TOOL_RESULT_TRUNCATE_BYTES, THREAD_TURN_CHUNK_BYTES,
85};
86pub use result_cache::{
87    CacheBudget, CacheFlushPolicy, CachePutOpts, CacheStats, CachedValue, SemanticCachePutOpts,
88    DEFAULT_BATCH_MAX_AGE, DEFAULT_BATCH_MAX_DIRTY, DEFAULT_MAX_BYTES, DEFAULT_MAX_ENTRIES,
89    DEFAULT_SEMANTIC_THRESHOLD,
90};
91pub use session::{Role, Session, Turn};
92#[doc(hidden)]
93pub use store::BudgetReport;
94pub use store::{
95    CompactReport, Mnemo, MnemoConfig, RecallRequest, RecallResponse, RecallResult, SnapshotInfo,
96    Stats, INDEX_PERSIST_EVERY_CHANGES, INDEX_PERSIST_EVERY_COMMITS,
97};
98pub use transcript::{read_transcript, TranscriptFormat};
99
100/// Re-export of [`ulid::Ulid`], the identifier type used for memories.
101pub use ulid::Ulid;
102
103// --- Fsync counter -----------------------------------------------------
104//
105// Instrumentation plumbing for the Phase 6 top-3 cost #2 fix: the
106// planner wants to know how many `sync_all` calls each `flush()` issues
107// before deciding whether to batch any of them. Every fsync call site
108// bumps a thread-local counter; `Mnemo::flush` snapshots it before and
109// after, exposing the delta on `Stats::fsyncs_last_flush`. Thread-local
110// so handles on separate threads don't poison each other's counts; the
111// engine's one-writer-at-a-time locking means a single handle sees a
112// coherent stream.
113
114mod fsync_counter {
115    use std::cell::Cell;
116
117    thread_local! {
118        static COUNT: Cell<u64> = const { Cell::new(0) };
119    }
120
121    /// Call once per `sync_all`.
122    pub(crate) fn bump() {
123        COUNT.with(|c| c.set(c.get().wrapping_add(1)));
124    }
125
126    /// Monotonic counter snapshot for this thread.
127    pub(crate) fn count() -> u64 {
128        COUNT.with(|c| c.get())
129    }
130}
131
132pub(crate) use fsync_counter::{bump as bump_fsync, count as fsync_count};
133
134/// Build-time cryptographic backend information.
135///
136/// Populated at compile time from the active `cfg` flags. On ARMv8
137/// builds compiled with `--cfg aes_armv8 --cfg polyval_armv8` (set by
138/// the repo's `.cargo/config.toml` and both CI workflows) the `aes`
139/// and `polyval` crates emit hardware AES + PMULL instructions; without
140/// those flags they fall back to a constant-time software path that
141/// runs at roughly 5× the cost per page.
142///
143/// Published through [`build_info`], exposed by `mnemo info` on the
144/// CLI and `mnemo.build_info()` from the Python binding. The point is
145/// verification — nobody should have to profile a release build to find
146/// out whether it's running software AES.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct BuildInfo {
149    /// Package version (`CARGO_PKG_VERSION`), e.g. `"0.5.1"`.
150    pub version: &'static str,
151    /// Target triple this binary was built for, e.g.
152    /// `"aarch64-apple-darwin"`.
153    pub target: &'static str,
154    /// Human-readable AES backend. One of:
155    /// - `"armv8 hardware"` — ARM with `cfg(aes_armv8)`
156    /// - `"armv8 software"` — ARM without the cfg; constant-time fallback
157    /// - `"x86_64 AES-NI (runtime detect)"` — x86_64 with runtime
158    ///   AES-NI detection in the `aes` crate
159    /// - `"portable software"` — any other target
160    pub aes_backend: &'static str,
161    /// Human-readable PMULL/CLMUL backend used by GHASH (part of AES-GCM).
162    /// Same shape as `aes_backend` but for `polyval`.
163    pub polyval_backend: &'static str,
164}
165
166/// Report this build's cryptographic backend configuration.
167///
168/// Cheap — all fields are `&'static str` populated at compile time.
169/// Safe to call from a hot path if, for some reason, that's useful.
170pub fn build_info() -> BuildInfo {
171    BuildInfo {
172        version: env!("CARGO_PKG_VERSION"),
173        target: TARGET_TRIPLE,
174        aes_backend: aes_backend_name(),
175        polyval_backend: polyval_backend_name(),
176    }
177}
178
179/// Target triple baked in via the `TARGET` env var that `cargo` sets
180/// for the host compile. Falls back to `"unknown"` if the var isn't
181/// set at build time (shouldn't happen under cargo, but a build.rs-free
182/// implementation keeps this crate build-script-free, so we use a
183/// cfg-based dispatch as the second-best).
184const TARGET_TRIPLE: &str = {
185    // `env!("TARGET")` isn't set by cargo for crates; it's set by rustc
186    // only for `std` builds. We approximate from `target_arch` /
187    // `target_os` cfgs so this stays build-script-free. Good enough
188    // for an info line; precise triples are a `cargo` or build.rs job.
189    if cfg!(all(target_arch = "aarch64", target_os = "macos")) {
190        "aarch64-apple-darwin"
191    } else if cfg!(all(target_arch = "aarch64", target_os = "linux")) {
192        "aarch64-unknown-linux-gnu"
193    } else if cfg!(all(target_arch = "x86_64", target_os = "macos")) {
194        "x86_64-apple-darwin"
195    } else if cfg!(all(target_arch = "x86_64", target_os = "linux")) {
196        "x86_64-unknown-linux-gnu"
197    } else if cfg!(all(target_arch = "x86_64", target_os = "windows")) {
198        "x86_64-pc-windows-msvc"
199    } else {
200        "unknown"
201    }
202};
203
204const fn aes_backend_name() -> &'static str {
205    if cfg!(aes_armv8) {
206        "armv8 hardware"
207    } else if cfg!(target_arch = "aarch64") {
208        // Software fallback is the dangerous case this whole mechanism
209        // exists to surface — flag it clearly so `mnemo info` makes it
210        // easy to notice a build picked up the slow path by mistake.
211        "armv8 software (constant-time fallback)"
212    } else if cfg!(target_arch = "x86_64") {
213        // `aes` 0.8.4 on x86_64 picks AES-NI vs the software path at
214        // runtime via the `cpufeatures` crate, so we can't tell from
215        // cfg alone which path a given invocation takes. In practice
216        // every x86_64 CPU shipped since ~2010 has AES-NI.
217        "x86_64 AES-NI (runtime detect)"
218    } else {
219        "portable software"
220    }
221}
222
223const fn polyval_backend_name() -> &'static str {
224    if cfg!(polyval_armv8) {
225        "armv8 PMULL hardware"
226    } else if cfg!(target_arch = "aarch64") {
227        "armv8 software (constant-time fallback)"
228    } else if cfg!(target_arch = "x86_64") {
229        "x86_64 CLMUL (runtime detect)"
230    } else {
231        "portable software"
232    }
233}
234
235/// Test-only crash-injection surface used by the Phase 1.3 proptest
236/// (`mnemo/tests/proptest_store.rs`). Compiled in under `cfg(test)`
237/// and the `failpoints` feature only; absent from release builds.
238///
239/// **Not part of the stable public API.** The double-underscore
240/// convention makes the "internal" contract obvious at the call site.
241#[cfg(any(test, feature = "failpoints"))]
242#[doc(hidden)]
243pub mod __failpoints {
244    /// Arm the pager and WAL commit path to fail on the Nth
245    /// subsequent write. `n = 0` fails the very next write; `n = -1`
246    /// (or any negative) disables. One-shot — disarms on trigger.
247    ///
248    /// Every write is checked before the byte hits the file, so a
249    /// triggered failure is guaranteed torn-BEFORE, never
250    /// torn-AFTER — critical for the "either pre-flush or post-flush"
251    /// invariant the property test asserts.
252    pub fn set_writes_until_fail(n: i64) {
253        crate::pager::failpoints::set_writes_until_fail(n)
254    }
255}
256
257/// WAL entry points for the PERF-1 allocation test
258/// (`tests/wal_recover_alloc.rs`), which needs its own global allocator and
259/// so can't live in this `forbid(unsafe_code)` crate. Not a stable API.
260#[doc(hidden)]
261pub mod __wal {
262    /// Page size of the WAL region.
263    pub const PAGE_SIZE: usize = crate::format::PAGE_SIZE;
264
265    /// One WAL frame: a home page number and its page image.
266    pub type Frame = (u64, Vec<u8>);
267
268    /// Bytes a transaction of `n` page frames occupies.
269    pub fn txn_byte_len(n: usize) -> u64 {
270        crate::wal::txn_byte_len(n, PAGE_SIZE)
271    }
272
273    /// Commit `frames` as transaction `txn_id` at the start of a region.
274    pub fn commit(
275        file: &mut std::fs::File,
276        wal_pages: u64,
277        txn_id: u64,
278        frames: &[Frame],
279    ) -> crate::Result<()> {
280        crate::wal::commit(file, 0, wal_pages, txn_id, frames)
281    }
282
283    /// Run open-time recovery over a region starting at page 0; returns the
284    /// frames to replay, if any.
285    pub fn recover(
286        file: &mut std::fs::File,
287        wal_pages: u64,
288        wal_seq: u64,
289    ) -> crate::Result<Option<Vec<Frame>>> {
290        crate::wal::recover(file, 0, wal_pages, wal_seq)
291    }
292}
293
294/// Internal parse surfaces exposed for cargo-fuzz targets (Phase 1.2).
295///
296/// **Not part of the stable public API.** Each function feeds an
297/// arbitrary byte slice into a parse/decode path that a hostile
298/// `.mnemo` file (or a hostile MCP client) can send us, and returns
299/// `Result<()>` so a fuzz target only asserts "no panic." Marked
300/// `#[doc(hidden)]` so `cargo doc` doesn't advertise it; do not import
301/// from application code.
302#[doc(hidden)]
303pub mod __fuzz {
304    /// Parse a `.mnemo` header page (page 0, plaintext) from arbitrary
305    /// bytes. This is the pre-passphrase attack surface — a hostile
306    /// file hits it before any DEK exists.
307    pub fn parse_header(bytes: &[u8]) -> crate::Result<()> {
308        crate::format::Header::from_page(bytes).map(|_| ())
309    }
310
311    /// Replay the WAL scan over an arbitrary byte region. Delegates
312    /// to [`crate::wal::recover_bytes`]; a corrupt / truncated /
313    /// adversarial region must return `Err` or `Ok(None/Some(_))`,
314    /// never panic.
315    pub fn wal_recover_bytes(region: &[u8], wal_seq: u64) -> crate::Result<()> {
316        crate::wal::recover_bytes(region, wal_seq).map(|_| ())
317    }
318
319    /// Decode a `Memory` record body from arbitrary bytes.
320    pub fn decode_memory(bytes: &[u8]) -> crate::Result<()> {
321        rmp_serde::from_slice::<crate::Memory>(bytes)
322            .map(|_| ())
323            .map_err(|e| crate::MnemoError::Serialize(e.to_string()))
324    }
325
326    /// Decode a `CacheEntry` record body from arbitrary bytes. Type is
327    /// `pub(crate)`, so this wrapper hides it behind a unit return.
328    pub fn decode_cache_entry(bytes: &[u8]) -> crate::Result<()> {
329        rmp_serde::from_slice::<crate::result_cache::CacheEntry>(bytes)
330            .map(|_| ())
331            .map_err(|e| crate::MnemoError::Serialize(e.to_string()))
332    }
333
334    /// Decode a cache-directory page-run body (a `Vec<CacheDirectoryEntry>`)
335    /// from arbitrary bytes.
336    pub fn decode_cache_directory(bytes: &[u8]) -> crate::Result<()> {
337        rmp_serde::from_slice::<Vec<crate::result_cache::CacheDirectoryEntry>>(bytes)
338            .map(|_| ())
339            .map_err(|e| crate::MnemoError::Serialize(e.to_string()))
340    }
341
342    /// Parse a single MCP stdio JSON-RPC request line. Malformed
343    /// input returns `Err(String)`; the fuzz target only asserts
344    /// no panic and that the server would not die on this line.
345    pub fn parse_mcp_request_line(line: &str) -> Result<(), String> {
346        crate::mcp::__fuzz_parse_request_line(line)
347    }
348}