What mnestic adds
mnestic is a maintained fork of CozoDB,
continued under MPL-2.0 from upstream commit 481af05 (the last upstream commit,
2024-12-04). The query language (CozoScript) and engine semantics are
unchanged unless noted here — what changed is performance, correctness, and
the primitives an agentic-memory workload actually needs.
mnestic
The pages in this section document behavior that is specific to mnestic. Pages elsewhere in these docs describe the engine inherited from CozoDB and apply to both.
Lineage
The fork point is CozoDB main HEAD — 30 commits ahead of the published
0.7.6 crate, which means adopting mnestic already gives you several unreleased
upstream fixes for free (including the stored_prefix_join correctness fix).
The version was bumped to 0.8.0 to mark the fork's identity rather than
0.7.7.
Highlights by release
0.13.1 — the count rewrite ships on
A patch release. The factorized count() rewrite, opt-in since 0.10.5 while the
planner-regression suite was built out, is now on by default: an eligible
single-clause count()-over-a-positive-join is counted without materializing the
join. Answers are identical by construction — the pass fires only on a provably
exact decomposition and leaves declined queries byte-identical — and
set_query_factorization(false) restores the previous behaviour. Measured on LSQB
sf0.1 (SQLite, release), both paths returning LDBC's published count: q1
72.4 s → 1.05 s (~69×), q6 42.1 s → 0.31 s (~134×).
Shipped alongside it: 0.13.0's expected-token parse hints, which pest 2.8 had
silently disabled for anyone resolving dependencies fresh (tracking became opt-in;
our caret requirement admitted 2.8; the committed lockfile meant CI never saw it).
The floor is now pest = "2.8", the tracking is enabled only around a re-parse of
an already-failed script — it is a process-wide global with a real cost, and an
embedded engine should not leave it on inside its host — and a weekly fresh-resolve
CI lane now covers the class.
0.13.0 — the correctness union, and the features it earned
A nine-bug hardening pass over the storage and index paths, paired with the primitives an agentic-memory workload needs. Ships with mnestic-rocks 0.1.10.
- The RocksDB table options you configure now reach RocksDB.
open_dbloaded yourBlockBasedTableOptions(block cache, block size,cache_index_and_filter_blocks) and then silently threw them away, resetting the table factory to a default-constructed one — so an embedded engine ran with an 8 MB read cache and a 4 KB block size regardless of what its host asked for. Fixed in mnestic-rocks 0.1.10. Every read-path benchmark taken before this measured a slower engine than mnestic actually is. Inherited from upstream Cozo. - Budgeted hybrid retrieval in one call.
hybrid_searchgains a budgeted-expansion mode — a graph leg withmax_nodesrunsBudgetedTraversal's cheapest-first weighted expansion over a cachedgraph:projection, seeded from the vector/FTS legs' own top-k — and itsvector_index/fts_indexare now optional, so any non-empty subset of legs can be fused. A payload missing its leg is a loud error, not a dropped signal. - A datetime standard library (
dt_*) with a typed validity bridge. Component extractors,dt_trunc, calendar-awaredt_add/dt_diff, strftimedt_format, anddt_to_validity— which converts float Unix seconds to aValiditywhere the unit is known.@and:as_ofnow accept aValidity-typed expression, closing the seconds-vs-microseconds trap 0.12.2 opened the door on. - The
!=inclusion–exclusion count rewrite is restored, behind a type gate (default OFF). Sound this time — it fires only when both inequality operands are variant-stable stored columns of one agreed type, so join equality andop_neqcannot disagree. LSQB q6: 41.7 s → 0.30 s (~140×), count exactly the published oracle. - Errors that name what the engine expected. Parse errors point their caret at
the deepest position reached and list the tokens that would have parsed;
index-search diagnostics carry the failing index kind's own code; and
import_from_backuprefuses a schema mismatch — the one path that could write a value violating its column's declared type.
→ Hybrid retrieval · Datetime functions · Release history
0.12.2 — a float in a validity is now an error
Validity and transaction-time stamps are integer microseconds since the Unix
epoch; now() and parse_timestamp() return float seconds. The engine
coerced one into the other silently, in all four places a timestamp can enter a
validity — the @ selector, the @ (tt: …) / :as_of selector, the
validity(...) constructor, and the write path. A fact meant for 2024 was
denominated a million times too small and stored at 1970: the row reads back
correctly on an ordinary query and is wrong only under time travel, which is
exactly where a bitemporal engine has to be trustworthy. On the read side the
misread landed before any row was asserted, so the query returned zero rows and
no error. All four now reject a float with a message that names the unit and
the fix (to_int(<expr> * 1000000)).
Like the 0.12.1 bugs below, this one is inherited — and it predates
bitemporality. Three of the four sites are verbatim upstream code at the fork
point (481af05, 2024-12-04): the @ selector (expr2vld_spec), the
validity(...) constructor (op_validity), and — worst — the write path itself,
whose DataValue::List arm of ColType::Validity is byte-identical to upstream's.
So is the accessor all three funnel through, Num::get_int, which coerces any
whole-numbered float to an i64. Only the transaction-time selector is ours, and 0.10.0 did not introduce
the coercion there, it inherited it: Validity columns and @ time travel are a
Cozo feature that predates the fork by years. Every CozoDB database with a
Validity column has this bug, and the upstream code that writes 1970 still ships.
The idiom that triggers it — Validity default [floor(now()), true] — had
exactly one caller we could find anywhere: upstream's own HNSW test, which we
inherited unchanged, which had been writing 1970 into upstream's own valid-time axis
for as long as the test existed, and which never asserted on the value. See
Release history for the upgrade action, and
Time travel for what the selector
accepts.
0.12.1 — six inherited bugs, found and named
Every bug in this release is inherited from upstream CozoDB and has been latent since before the fork point — not one is a regression the fork introduced. They surfaced in a line-by-line audit of the engine we inherited. Naming them is the point: a dormant project ships none of these fixes, because nobody is reading the code.
::reindex <relation>— the repair path. Rebuild a relation's HNSW / FTS / LSH indexes in place from the index configuration the database already stores. It replaces the "drop and recreate the index" advice the bulk-load paths used to give, which meant reconstructing the original::hnsw/::ftscreation script (extractor, tokenizer, filters,ef_construction,m_neighbours…) by hand. Each index is rebuilt from its own stored manifest — an LSH manifest keeps the derived band geometry but not the weights behind it, so a hand-rolled recreate silently changes the index's recall profile.- Full-text postings leaked on in-place updates (affects every release through
0.12.0). A relation carrying only an FTS index never deleted a row's old
postings on a
:putover an existing key: terms the document no longer contained kept matching it, the index grew without bound, and BM25 statistics drifted — a measured 55% score error. Existing indexes are already affected; the fix stops new leakage but cannot evict what was written, so affected relations need one::reindex. LSH does not leak. - The transaction API reported success for a failed commit.
MultiTransaction::commit()discarded the commit's own result, so a failed commit returnedOk— and the HTTP/transactendpoint answered200 {"ok": true}for transactions that never committed. Change-feed subscribers likewise received events for rows that were never committed. - Two non-default backends fixed.
newrocksdbnever armed conflict validation, so concurrent updates were silently lost;sled'sdel()never deleted (upstream #306). Both now run a transaction-contract suite in CI. import_from_backupsilently stranded HNSW/FTS/LSH indexes — a restored backup returned nothing from hybrid retrieval, with no signal. It now warns, and both bulk-load warnings point at::reindex.
→ System ops · Proximity search · Release history
0.12.0 — budgeted weighted traversal
BudgetedTraversal— the context-fill primitive. A newgraph-algofixed rule: cheapest-first expansion from a set of seeds, over non-negative edge weights, under a required global budget of distinct nodes (max_nodes), emitting the budget's-worth of cheapest admissible nodes as(node, cost, parent, depth). Deterministic by construction — admission follows a(cost, node)total order, and positional edges vs. a cachedgraph:projection produce byte-for-byte identical output.max_costbounds admissible path cost;max_depthis an exact hop bound (layered labels, never depth-pruned Dijkstra). An optional gate relation plus anadmit:predicate filters mid-expansion — a gated-out node spends no budget and never bridges — and the loop honors:timeout/::kill. Weights are consumed as costs; monotone transforms like-ln(weight)are the caller's. Measured at the release's merge gate: one call over a cached projection runs 2–4× faster than the production host-side BFS it replaces.- The optional
rayondependency is bounded>=1.10, <1.11— rayon 1.11 breaksgraph_builder0.4.x (the CSR-builder crate behindgraph-algo), so a fresh downstream resolve now lands on a working pair.
→ Utilities & algorithms · Graph projections
0.11.1 — built-in skyline aggregates
pareto_min/pareto_maxkeep the whole Pareto frontier, per group. Given a numeric vector per row, each keeps the non-dominated points under componentwise order (pareto_min= smaller is better on every component,pareto_max= larger), one row per survivor — a query can surface a contested set of equally-good answers instead of collapsing to a single min or max. Mixed objectives (minimize price, maximize quality) use the sign-flip idiom: negate the maximized components and usepareto_min.- Native dominance — no host registration. Unlike the registered
antichainbounded-meet from 0.10.1, the componentwise comparison is built in, so these need noregister_bounded_meet_aggrand are reachable from every binding — the PyPI wheel,cozo-bin, langchain, llama-index — through plainrun_script. They compose in recursive rules too, and a malformed operand (non-list, non-numeric or NaN component, empty vector) is a loud error.
→ Skyline aggregates · Aggregations
0.11.0 — cached graph projections
- Name a graph once and reuse it across queries.
::graph create g { edges: knows, nodes: person }builds a named, in-memory adjacency over stored relations that twelve graph algorithms reuse via agraph:option, instead of scanning the edges and rebuilding the CSR on every call. On a 400,000-edge graph, reusing the projection tookConnectedComponentsfrom 127 ms to 7.9 ms (16×),PageRankat 20 iterations from 150 ms to 10 ms (15×), andClusteringCoefficientsfrom 169 ms to 56 ms (3×) — what is cached is the setup, so the gain shrinks as the kernel dominates. The projection is always fresh — it never serves a transaction data that differs from its own scan of the sources, and writing to a source frees what was built from it; under write churn it degrades to build-per-query, never stale. Projections are in-memory and not persisted, and a 512 MiB LRU ceiling (settable from Rust and Python;0disables caching) bounds their footprint. - BREAKING (results):
PageRank's defaultiterationsis now 20, up from 10 — a below-upstream default that is measurably non-convergent at the defaultepsilon. Passiterations: 10to restore the old numbers.PageRankalso now accepts an optional node relation (asConnectedComponentsalready did), so edge-less vertices are ranked instead of silently dropped, and warns when the iteration cap stops it short ofepsilon. - Fix: an empty edge relation no longer aborts the process in seven graph
algorithms (it used to panic; they now return no rows, as
PageRankalready did). Andmulti_transactionno longer deadlocks a process by parking arayonworker for the transaction's whole lifetime — it now runs on a dedicated thread, which affects every caller of that API.
→ Graph projections · Utilities & algorithms · System ops
0.10.7 — a join-reorder plan fix, and factorization from Python
- Fix: the greedy join reorder no longer demotes a full-composite-key filter
to a partial-key expansion. A tie-break bug (
full_key_lookup_bonus) could pull a high-fan-out edge ahead of a more selective atom and regress a cyclic-join query (a benchmarker measured LDBC-SNB LSQB Q3 go from ~19s to a timeout; the fix restores it). Result sets are unchanged and the min-new-vars speed-up from 0.10.5 is preserved. Separately, the Python binding now exposesdb.set_query_factorization(True)/db.query_factorization(), so the 0.10.5 factorized-count()kill switch — previously Rust-only — is toggleable from Python (default still off).
0.10.6 — legacy databases open again after upgrade
- Fix: relation catalogs written before 0.10.0 no longer fail to open. An
urgent upgrade-safety patch. On 0.10.0–0.10.5 a database created before 0.10.0
(or last updated by an index/rename/destroy path) could fail to open with
"Cannot deserialize relation metadata from bytes" — the bitemporality work
added a
RelationHandlefield mid-struct, which broke positional decoding of the older relation-catalog layout and could take the whole database down. 0.10.6 makes catalog decoding tolerant of the legacy layout and switches every catalog write to a self-describing, field-named encoding so it can't recur. No migration — legacy databases open as-is and re-canonicalize on their next write. Anyone who upgraded a pre-0.10.0 database to any of 0.10.0–0.10.5 should upgrade. Two internal items ride along: the deterministic join-reorder pass is refactored to a pure function with no behavior change, and thestorage-rocksdbPython-wheel CI is hardened.
0.10.5 — interruptible queries, wall-clock budgets & a stat-free join reorder
::killand:timeoutnow actually interrupt a running query.::runningand::killdispatch before opening a storage transaction, so a::killno longer queues behind the very query it is trying to stop (the old failure on the mem/SQLite backends). The per-query poison flag is now checked every 4096 pulls inside the relational-algebra enumeration, so even a long single-rule join that emits no rows is finally interruptible. The Pythonclose()no longer raises "Already borrowed" against a liverun_script.- Per-query wall-clock budget — set a deadline three ways: the in-script
:timeout <secs>option, a per-callrun_script_with_options, or a Db-wideset_default_query_timeout. The effective deadline is the minimum of whichever are set — a:timeoutcan only tighten the budget, never extend past the default. Expiry raises a distincteval::timeouterror (a::killraiseseval::killed); Pythonrun_scriptgains atimeout=kwarg. Wasm carries no wall-clock budget (no monotonic clock there). - Deterministic greedy join reorder — default on — a stat-free, min-new-vars
reorder of a conjunction's positive relation atoms, removing the N³ blow-up a
naively-ordered query (the kind an LLM authors) falls into (measured 54.5× on
the repro; N³ → N²). Results are unchanged — it is the identity on any
already-greedy order, and it is not a cost-based optimizer. Opt out per query
with
:reorder written; a residual Cartesian step is warned and annotated in::explain. - Automatic factorized
count()rewrite — opt-in, default off — behindset_query_factorization, an eligible single-clausecount()-over-join is rewritten into per-key counting sub-rules that compute a bit-identical (exact-i64,Int-typed) answer without materializing the join (the benchmark measured 4–342× versus a factorizing optimizer). It fires only on shapes it can prove exact; an always-on detector adds a factorization advisory to::explain. - RocksDB now ships in the PyPI
mnesticwheel —CozoDbPy("rocksdb", path)works straight frompip install mnestic(wascompact/SQLite-only). The sdist stays compact, so the persistent engine is wheel-only. - Bulk
import_relationsinto an index-bearing relation now warns — the bulk path maintains B-tree indexes but not HNSW/FTS/LSH, so imported rows stay invisible to vector/text search until the index is rebuilt; a warning now flags it.
→ Interruptibility & timeouts · Join reorder · Factorized counting
0.10.1 — antichain aggregate & interval primitives
- Dominance bounded-meet — the antichain / skyline aggregate —
register_bounded_meet_aggropens the bounded-meet category to a host-supplied strict partial order, keeping the non-dominated (Pareto-frontier) set of operands per group, one output row each.max_survivorsis a mandatory resource guard — overflow is a loud error, never a silent truncation. Rust-embedded only in v1 (host closures do not cross the Python/served surfaces). - Interval primitives —
interval_overlaps(a, b)builtin and theinterval_coalesce(span)aggregate over half-open[start, end)list intervals. Touching spans do not overlap but do coalesce ([0,5)+[5,10)=[0,10)); empty spans overlap nothing; malformed spans are loud errors, never silent falses. - Correctness fix — the
bit_and/bit_ormeet aggregates now report whether the value actually changed, so a non-changing fold no longer re-enters the semi-naive delta every epoch (the same defect family as the 0.10.0and/orchanged-bit fix).
0.10.0 — bitemporality & provenance semirings
- Bitemporality — engine-assigned transaction time alongside Cozo's valid
time. A crash-safe monotone commit clock stamps every write to a
TxTimerelation; reads default to the current belief and time-travel with an@selector or the:as_ofquery option;::history/::history_gc(persisted floor) /::evict(audited hard deletion) manage the record's lifecycle. Answers "what did we believe at time T about period Y" in-engine, with current-belief reads within ~4–12% of the single-axis baseline. - Provenance semirings — the same recursive rules can compute existence, cost,
confidence, or evidence.
register_custom_aggradmits user-defined absorptive combines into recursion;min_cost_kis a bounded-meet aggregate returning the k best derivations per answer with the evidence chains that justify them; and:reconcileis recompute-based belief revision that keeps derived annotations consistent under base-fact retraction, composed with the tt axis. - Four upstream bugs fixed along the way — the inverted changed-bit in
and/ormeet aggregates, a panic on negated validity atoms, wrong answers from prefix-truncated temporal-column joins, and the braced-%returnimperative parse panic.
→ Bitemporality · Provenance semirings
0.9.0 — read-only Cypher & corrupt-DB tooling
- Read-only Cypher query surface (alpha, opt-in
cypherfeature, off by default) — translate a subset of openCypher to CozoScript so the engine can be evaluated without first learning Datalog. New API:run_cypher/cypher_to_script, driven by a caller-supplied schema mapping the property-graph model onto stored relations. Datalog stays the native, full-power language; this is a read-only on-ramp (no write clauses). The published PyPI wheel ships without it. ::repair_corrupt <relation>— surgically deletes truncated tuples (short value bytes from interrupted writes) by their intact store keys, giving applications an alternative to dropping a database that fails integrity checks.- Non-panicking
::index create— a single truncated tuple used to panic the whole index build (and could make a database unopenable); corrupt tuples are now skipped with a loud error naming the relation, index, and arity mismatch. Both were banked as 0.8.6 and first published here.
0.8.5 — flat parallel HNSW builds & snapshot reads
- Flat in-RAM parallel HNSW bulk build —
::hnsw createnow builds the graph in contiguous integer-indexed memory (the hnswlib/pgvector/Lucene layout) and inserts in parallel with per-node locks, eliminating the tuple encode/decode and allocator traffic that dominated the old temp-store build.MNESTIC_INDEX_BUILD_THREADScontrols worker count. - Plain-snapshot read path for read-only scripts (RocksDB) — immutable scripts no longer open a pessimistic transaction; they read the base DB through a plain MVCC snapshot with no lock-manager bookkeeping (keyed point read p50 28.5 → 23.9 µs, −16%, measured).
- Batched HNSW neighbour reads — the search path fetches unvisited neighbours'
vectors through one RocksDB
MultiGetper expansion step instead of a serial point-get per neighbour; the win case is cold-cache / larger-than-RAM data.
0.8.4 — per-leg retrieval detail & an FTS concurrency fix
- Per-leg retrieval detail —
ReciprocalRankFusion(..., detailed: true)andHybridSearch::detailedswitch the output to the long format[item, fused_score, list_id, leg_rank, leg_score], one row per contributing list — the mechanism behind a "why was this retrieved" surface, reconstructing the fused score exactly. Exposed to the Python binding asdetailed: True. - FTS
avgdlconcurrency fix — 0.8.3's durable doc-stats counter was one shared storage key rewritten inside every document transaction, making concurrent writers to an FTS-indexed relation conflict on a single row lock (and losing updates through an unlocked read-modify-write). The counter is now process-cached and scan-seeded, with no shared storage key in the hot path; per-queryavgdlstays O(1).
0.8.3 — native 3-way fused recall & BM25 full-text
- Native 3-way fused recall —
hybrid_searchnow takes typedGraphLegs, so graph proximity fuses with the vector and keyword legs in one call. Each leg expands from seeds over a stored edge relation up tomax_hopsand ranks reached nodes by minimum hop distance. The fused call runs all three signals at ~41.55 ms p50, roughly 4× faster than hand-decomposing it into separate queries. - BM25-correct full-text search — the default
::ftsscorer is now Okapi BM25 (term-frequency saturation + document-length normalization),ORsums per-term contributions, andavgdlis an O(1) durable counter rather than a per-query scan. This lifted fused recall from 0.75 to 0.954.tfandtf_idfremain selectable for byte-identical upstream scoring.
Caution
Behavior change in 0.8.3: the default ::fts score kind moved from tf_idf
to bm25. Pass score_kind: 'tf_idf' (or 'tf') to keep upstream scoring.
→ Hybrid retrieval (RRF + MMR + graph legs)
0.8.2 — non-blocking HNSW index builds
Building an HNSW index no longer holds the base relation's write lock for the
duration of the build, so concurrent reads no longer stall for minutes. The
graph is built off-lock under a snapshot and bulk-published with RocksDB's
SstFileWriter / IngestExternalFile.
0.8.1 — one-call hybrid retrieval & faster builds
hybrid_searchruns HNSW + FTS (+ optional graph traversal), fuses with Reciprocal Rank Fusion, and optionally diversifies with MMR — in a single typed call, replacing roughly seven hand-written Datalog rules.- HNSW index builds ~3× faster (20k × 128: 135 s → 43.6 s, measured release), with a byte-identical result.
- The C++/RocksDB bridge is now a maintained fork,
mnestic-rocks, keeping the importable namecozorocks.
→ Hybrid retrieval (RRF + MMR)
0.8.0 — fixes & agentic-memory primitives
- Equality pushdown —
*rel[k, ..], k == <value>now compiles to a keyedstored_prefix_joininstead of a full scan (~28–29× faster single-row primary-key lookups at 5k rows, measured). ReciprocalRankFusion/MaximalMarginalRelevancefixed rules (aliasesRRF/MMR) — the fusion and diversity primitives behind hybrid retrieval, usable directly in Datalog.- ULID functions —
rand_ulid()andulid_timestamp()for lexicographically-sortable, time-ordered keys. - Parser fix — identifiers that begin with a keyword literal
(
nullable_column,trueValue,falsey) now parse correctly.
→ Equality pushdown · ULID identifiers
Not affiliated with CozoDB
mnestic is not the official CozoDB and is not affiliated with or endorsed by its authors. All credit for the original design belongs to Ziyang Hu and the Cozo Project Authors. See License & attribution.