# grit
[](https://github.com/bofeizhu/grit/actions/workflows/ci.yml)
[](https://crates.io/crates/grit-core)
[](https://docs.rs/grit-core)
[](#license)
An embedded, **bi-temporal property graph for agent memory**. One SQLite
file, in-process, written in Rust. No server, no daemon, no network —
compiles for macOS, Windows, Linux, iOS, and Android.
grit is the piece of grit at the center of the
[pearl](https://github.com/bofeizhu/nacre#why-this-exists) stack — Layer 1,
doing deterministic graph **storage and hybrid retrieval** only. The LLM
extraction pipeline that deposits memory around it is
[nacre](https://github.com/bofeizhu/nacre) (Layer 2); the agent app is
Layer 3. The full design contract — invariants, scope limits, testing bar —
is [AGENTS.md](AGENTS.md), and it is binding for anyone (human or agent)
working here.
```rust
use grit_core::{Budget, GraphOp, Grit, Options, Query, Traversal};
let g = Grit::open("memory.db", Options::new("laptop"))?;
g.apply(GraphOp::AddEpisode { /* provenance: a chat turn, a doc chunk … */ })?;
g.apply(GraphOp::AddEdge { /* a fact, with event-time validity … */ })?;
// Hybrid recall: BM25 + vectors + graph expansion, RRF-fused, budgeted.
let hits = g.search(Query::text("exactness").group("algebra").budget(Budget::items(20)))?;
// Time travel is a query, not archaeology.
let then = g.traverse(&[node_id], &Traversal::default().as_of(march).as_at(march))?;
let past = g.node_history(node_id)?; // every edge ever believed
```
## Why on SQLite
The embedded-graph-database niche is a graveyard: the ideal engine is
archived, others are dormant, server-based, or license-encumbered — and
none of it runs inside an iOS app. Meanwhile the graph semantics agent
memory needs are a **recipe, not a product**. So grit is a deliberately
thin layer (~a few kLOC) on the one storage engine that is in-process,
iOS-native, public domain, and older than most databases. SQLite carries
the dangerous parts — durability, crash recovery, decades of hardening —
plus FTS5 and [sqlite-vec](https://github.com/asg017/sqlite-vec),
statically linked.
## Not a vector database
The default architecture for "agent memory" is chunks in a vector store.
grit is deliberately not that, and the trade is worth stating plainly.
A dedicated vector database — zvec, LanceDB, Qdrant, Milvus — optimizes one
operation: approximate nearest-neighbor search over millions of embeddings,
via ANN indexes and quantization. At that job it beats grit by orders of
magnitude and always will: grit's vector leg is an **exhaustive, exact
scan** (sqlite-vec), so query cost is O(corpus) where an ANN index pays
O(~log corpus). At ten million vectors that is the difference between
milliseconds and tens of seconds. grit does not enter that race — by
decision, not accident (see the performance envelope below).
What walking away from ANN buys, for the job grit actually has:
- **Recall 1.0, deterministically.** Exact scan means no approximation, no
insertion-order dependence, no tuning knobs. The same file returns the
same ranking — which is what makes retrieval *testable*: nacre's
golden-trace oracle and the `EXPLAIN QUERY PLAN` pins in CI both depend
on it. (FalkorDB's HNSW index makes upstream Graphiti's own retrieval
nondeterministic across runs; that bug class is structurally absent
here.)
- **Zero index lifecycle.** A fact is searchable in the transaction that
wrote it. No build step, no memory-resident graph, no degradation under
the upsert/invalidate churn agent memory actually does, nothing to
rebuild after a sync — and no second store to keep consistent with the
file.
- **Similarity is one leg, not the model.** Hits are RRF-fused from BM25
(word and CJK-trigram), vector cosine, and graph expansion — filtered
bi-temporally, with provenance attached. *"What did I believe in March,
and which conversation told me?"* is not expressible as k-NN over
chunks.
- **It runs where the agent runs.** One statically-linked SQLite file,
including on iOS/Android, where a database engine sidecar is a
non-starter.
The cost, measured rather than hand-waved: the exhaustive scan moves
~2.2 GB/s per query on Apple Silicon, so the 50 ms search budget covers
roughly 25k dim-1024 vectors in the queried group. Group-partitioned vec
tables (schema v5) keep real workloads — many moderate namespaces — inside
that line; a single group at the 1M-edge envelope top currently exceeds
it, with the numbers and the mitigation plan (binary quantization + exact
re-rank, still deterministic) recorded in
[docs/vector-leg-latency.md](docs/vector-leg-latency.md).
Past the envelope, use both: bulk retrieval over a million document chunks
belongs in a dedicated vector store; what the agent *learned* from those
documents belongs here. The layers compose — that is the
[pearl](https://github.com/bofeizhu/nacre#why-this-exists) architecture,
not a rivalry.
## The model
**Every mutation is a `GraphOp`**, appended to an op-log; graph tables are
derived state in the same transaction. The vocabulary is closed and small —
it is also the future sync unit:
| `AddNode` | create an entity (id-collision: lowest HLC wins, deterministically) |
| `AddEdge` | create a **fat edge** — the fact sentence lives on it, with event-time `valid_at`/`invalid_at` |
| `AddEpisode` | record raw provenance and its mentions — every fact traces to sources |
| `UpdateNode` | revise entity metadata (name/summary/kind/attrs) — per-field last-writer-wins registers; history stays in the op-log |
| `InvalidateEdge` | close a fact's event-time interval; concurrent invalidations converge to the earliest |
| `MergeNodes` | execute a dedup decision (Layer 2 decides *whether*; cycle-safe canonical resolution) |
| `Purge` | the only destructive op — exact-id right-to-forget, tombstoned, audited |
**Bi-temporal throughout.** Edges carry event time (when a fact was true in
the world) and system time (when it was believed). Invalidations are
belief-versioned, so *"back in March, did we think this job still held?"*
is `as_of`/`as_at` on an ordinary traversal.
**Sync-ready before sync exists.** UUIDv7 keys, hybrid logical clocks, and
op application that is idempotent and commutative for concurrent ops —
property-tested across adversarial interleavings (merge cycles, purges
racing adds, updates arriving before their nodes, out-of-order
invalidations). Embeddings stay out of the op-log: they're recomputable
local state tagged with a model identity, stored and served back through
typed getters/setters.
**Data outlives the library.** Lossless JSONL export/import of the full
graph + op-log is a permanent compatibility surface; schema migrations are
forward-only and tested against frozen fixture databases from every
released version.
## Proven by an oracle
grit's API is exercised by more than unit tests:
[nacre](https://github.com/bofeizhu/nacre)'s golden-trace conformance
replays recorded LLM/embedder responses through the full Rust stack and
diffs the resulting grit graph against **pinned Python Graphiti's** frozen
output — byte-equal facts, timestamps, attributions. Several grit 0.2
capabilities (`UpdateNode`, `AddEdge.invalid_at`, embedding getters, group
scans) exist because that oracle demanded them.
## Performance envelope
Scale target: **≤100k nodes / ≤1M edges** — years of a person's learned
knowledge, not a fleet's raw perception (extract first; that's the whole
architecture). Measured at v0.1: traversal (3 hops, validity-filtered,
256-node budget) holds its ≤10 ms p95 target with ≥2× margin at 100k
nodes / 300k edges; on a deliberately adversarial 1M-edge uniform-random
fixture it measures ~17 ms warm p95, enforced by a CI latency tripwire.
Criterion benches fail CI on >20 % regression.
## Non-goals
No server mode, wire protocol, or networking. No general Cypher (a parked,
fail-loud translator stub exists in `grit-compat`). No graph algorithms
(PageRank, community detection) without a proven need. No multi-process
access — one process, one writer. No chasing scale beyond the envelope.
Each of these is an explicit decision in AGENTS.md, reversible only as a
new decision.
## Workspace
| [`grit-core`](https://crates.io/crates/grit-core) | schema, op-log writer actor, traversal, hybrid search — the product |
| [`grit-compat`](https://crates.io/crates/grit-compat) | parked, fail-loud Graphiti/FalkorDB-dialect translator stub |
| `grit-cli` | dev tool: import/export, stats, ad-hoc search/traverse |
## Commands
```bash
cargo build --workspace
cargo test --workspace # unit + property + crash + migration + plan-pinning tests
cargo test -p grit-core --release --test envelope -- --ignored # latency tripwire
cargo bench -p grit-core --bench hot_paths # criterion
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all
```
The suite includes a `kill -9` crash harness (derived tables must equal a
full op-log replay after any crash point), op-log merge-law property tests,
frozen schema-version fixtures, and `EXPLAIN QUERY PLAN` pinning — an index
regression is a correctness bug at this latency budget. Everything runs
offline.
## License
MIT OR Apache-2.0, at your option. See [NOTICE](NOTICE) for attributions
(Graphiti schema concepts, simple-graph traversal templates, Cortex scoring
ideas).