Engrava
Memory frameworkOfficial Sovantica project
Created by Sovantica
Published in sovantica/engrava. Publisher page
12 GitHub stars
What it is
The memory database for AI agents - graph memory, hybrid search, tamper-evident thought/edge journal.
AutoGineer overview, from the brain’s own files
Engrava is an in-process memory database designed for AI agents, built on SQLite. It provides a structured way to store and retrieve agent thoughts using a graph-based model, where thoughts are linked by typed edges. The system supports hybrid search, combining vector similarity, full-text search (FTS5), recency, and priority. It includes built-in mechanisms for memory consolidation (dreaming), memory hygiene (archiving and garbage collection), and a tamper-evident journal for tracking mutations. Engrava operates as a library with no external service dependencies in its core path, allowing agents to maintain persistent, queryable state locally.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
Engrava uses a SQLite database to store thoughts and their relationships. It organizes data into thoughts and typed edges, which can be queried using the MindQL language. The system supports hybrid search by combining vector embeddings, keyword search, and metadata signals. It manages memory through a lifecycle that includes archiving and garbage collection, and it can consolidate memories into reflections. The system is extensible via hooks and provides an MCP server for integration with various AI agent frameworks.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 16 |
| docs | docs | 37 |
| examples | examples | 13 |
| scripts | scripts | 24 |
| src | source-code | 89 |
| tests | tests | 201 |
| .githooks | folder | 1 |
The agent reads
- CONTRIBUTING.md evidence: CLAUDE.md
Requirements
Runtimes: Python >=3.11
Databases: SQLite
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
This brain runs code automatically: 2 scripts. Review .githooks/commit-msg, Makefile before opening the folder in Claude Code. No step below turns a hook on for you.
.githooks/commit-msg:runs on git commit-msgMakefile:echo "$(BLUE)>> Installing engrava + dev dependencies$(RESET)"; $(PIP) install --upgrade pip setuptools wheel; $(PIP) install -e ".[dev]"; bash scripts/install_hooks.sh; echo "$(GREEN)>> Done. Run 'make check' to verify quality gate.$(RESET)"
- Not scanned by AutoGineerIts dependencies are installed by pip or uv when you run it and were not scanned by AutoGineer (5 declared). https://pypi.org/project/engrava/
Created by Sovantica. Published in sovantica/engrava. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Persistent memory for autonomous AI agents
- Personal knowledge base management
- Conversation storage with semantic retrieval
- Research notes with associative linking
- Memory-backed agent development
- Audit-trailed thought and edge tracking
- “Store a new observation about a project requirement.”
- The agent uses the remember() method to persist the thought, which is then indexed for future hybrid search and graph traversal.
- “Retrieve relevant context for a coding task.”
- The agent uses the recall() method, which performs a hybrid search across FTS5, vector, and graph signals to return ranked, contextually relevant thoughts.
- “Query the memory graph for all observations with high priority.”
- The agent executes a MindQL query: FIND thoughts WHERE thought_type = 'OBSERVATION' AND priority = 'P1'.
- “Consolidate old memories to free up space.”
- The agent triggers the DreamingExtension to cluster related thoughts and compute reflections, or uses the memory hygiene loop to archive low-signal thoughts.
Documentation by Sovantica
Engrava
The memory database for AI agents.
A queryable memory graph with bi-temporal valid-time predicates — in an in-process Python library over one SQLite file, with no generative-model call in the core write path.
Image: CI Image: PyPI Image: Python Image: License: MIT
Engrava is a standalone, in-process database for AI agent memory. Built on SQLite, it provides thought CRUD, edge-based knowledge graphs, embedding-based similarity search, full-text search (FTS5/BM25), and a declarative extension system — all in a single package with zero external service dependencies.
Benchmark results are published, and the runs are reproducible from a separate repository. Every
published run is Group A — memory_pipeline_llms: [], no language model anywhere in the memory
layer's built-in path. That is a property of the architecture's default signals and hooks rather
than a measurement, unless a custom Dreaming signal or Memory Hygiene hook is configured to call
one.
Benchmark results — the live table: every published row with the comparability segment it belongs to.
engrava-benchmark — the runner. Clone it and reproduce a result against the package from PyPI. MIT.
Discussions — questions; reproduction questions belong in the Q&A category.
Use Cases
AI agent persistent memory
Personal knowledge base
Conversation storage with semantic search
Research notes with associative linking
Any application that needs a thought-graph with embeddings
Quick Start
Installation
pip install engravaOptional extras:
pip install 'engrava[vec]' # sqlite-vec vector search backend
pip install 'engrava[embeddings-local]' # sentence-transformers embeddings (local model)
pip install 'engrava[embeddings-openai]' # OpenAI-compatible embeddings API
pip install 'engrava[embeddings-ollama]' # Ollama local embeddings server
pip install 'engrava[embeddings-hf]' # HuggingFace Inference API embeddingsDreaming/consolidation and the knowledge graph need no extra — they are part of the base install.
embeddings-localcarries a large first-run download. It pullssentence-transformersandtorch, plus a further, separate model download (~88 MB forall-MiniLM-L6-v2, cached under~/.cache/huggingface/hub) on first use. On Linux, the PyPI build oftorchalso pulls the NVIDIA CUDA libraries as dependencies: several gigabytes, most of it usable only with an NVIDIA GPU. Without one, install the CPU-only build first (see Configuration: No NVIDIA GPU for the exact command and numbers).engrava-mcp[local]is the exact same download, reached through the server package instead of this one. If you do not need semantic search in-process,pip install engravaalone (no extra, no download, no model) already gives you keyword search, the graph, and MindQL — see Configuration → Quick-start profiles for that and the Ollama-backed alternative that keeps the model out of this process entirely.
Basic Usage
Store a memory and search for it in two calls — no IDs to generate, no record to assemble:
import asyncio
import aiosqlite
from engrava import SqliteEngravaCore
async def main() -> None:
# SqliteEngravaCore wraps an open aiosqlite connection.
async with aiosqlite.connect(":memory:") as conn:
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn)
await store.ensure_schema()
await store.remember("Python is great for AI agents")
await store.remember("SQLite needs no server")
result = await store.recall("what language is good for agents?")
for thought_id, score in result.results:
thought = await store.get_thought(thought_id)
if thought is not None:
print(f"{thought.essence} (score: {score:.3f})")
asyncio.run(main())remember() stores the text as a thought (generating its ID for you) and
returns the stored ThoughtRecord; recall() runs the same hybrid search as
search_hybrid() and returns the ranked results. This store has no
SearchConfig of its own, and it still ranks with the same default weights —
FTS 0.30, vector 0.55, recency 0.10, priority 0.05, graph 0.00
(opt-in) — as a store built with one; see Hybrid
Search.
For full control — setting
priority, thought type, metadata, or the cognitive cycle on a write — build a
ThoughtRecord yourself and call create_thought().
From here, link thoughts with typed edges, query them with MindQL, or run the full ingest → dream → search tour in the Quick Start guide.
Configuration-Driven Setup
from engrava import SqliteEngravaCore
# from_config opens and OWNS the connection — use it as an async context manager.
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
# The schema is already applied by from_config.
thought = await store.get_thought("some-id")See docs/configuration.md for the full YAML schema.
Upgrading
Automatic schema migration runs on first connection. See the upgrade guide for compatibility notes, backup guidance, and troubleshooting steps.
Features
Thought CRUD
Create, read, update, and archive thoughts with full lifecycle management.
All models are frozen Pydantic objects — mutations happen via evolve().
Edge-Based Knowledge Graph
Link thoughts with typed, weighted edges. Edge types include ASSOCIATED,
DEPENDS_ON, DERIVED_FROM, CONSOLIDATED_FROM (created by dreaming), and
CONTESTED_BY.
Embedding Search
Store embeddings alongside thoughts and search them with the built-in NumPy
cosine backend or the optional sqlite-vec backend (pip install 'engrava[vec]'). Pluggable embedding providers:
| Provider | Extra | Backend |
|---|---|---|
SentenceTransformerProvider | embeddings-local | Local model via sentence-transformers |
OpenAICompatibleProvider | embeddings-openai | Any OpenAI-compatible API |
OllamaProvider | embeddings-ollama | Local Ollama server |
HuggingFaceProvider | embeddings-hf | HuggingFace Inference API |
CallbackProvider | (built-in) | Custom callable |
Full-Text Search (FTS5)
SQLite FTS5 virtual table with BM25 ranking. Hybrid search combines vector similarity, text relevance, recency, priority, and graph connectivity. Signals that cannot run for a query are skipped and the remaining weights are redistributed.
MindQL Query Language
Declarative query language for the thought-graph:
FIND thoughts WHERE thought_type = 'OBSERVATION' AND priority = 'P1' LIMIT 10
COUNT thoughts WHERE lifecycle_status = 'ACTIVE'
SELECT thought_id, essence FROM thought WHERE thought_type = 'BELIEF'Extensible with custom commands through MindQLExecutor or an
ExtensionManifest; the lifecycle hook registry is reserved and is not
consulted by the core executor.
Extension System
Plug into the thought lifecycle via EngravaHooksProtocol. Subclass
DefaultEngravaHooks when you only need selected active methods:
from engrava import DefaultEngravaHooks, ThoughtRecord
class MyHooks(DefaultEngravaHooks):
async def on_store(self, thought: ThoughtRecord) -> ThoughtRecord:
# Observe or enrich the object returned after persistence.
return thought
async def decay_function(
self, thought: ThoughtRecord, elapsed_cycles: int
) -> float:
# Supply a decay multiplier to an enabled Memory Hygiene pass.
return 1.0Core currently invokes on_store, on_retrieve, and decay_function.
on_store runs after the source thought's row is inserted; changing its
return value does not rewrite the persisted row. score_function and
mindql_extension_registry() remain reserved protocol methods and are not
called by core.
Dreaming / Memory Consolidation
Built-in DreamingExtension for periodic memory consolidation — scores
thoughts via its default signals (no LLM calls), promotes high-value
entries, and creates REFLECTION thoughts by clustering semantically
related thoughts and computing centroid embeddings through a deterministic
structural function, not an LLM. A custom signal you register with
DreamingExtension runs whatever code it contains. Available since 0.3.0.
→ See docs/benchmarks.md for reproducible
evidence (synthetic benchmark suite runnable in ~5 minutes).
Forgetting / Memory Hygiene
The subtractive half of memory maintenance, paired with Dreaming: an opt-in
loop whose built-in scoring makes no LLM calls, that archives cold,
low-signal thoughts — the default action, reversible via restore_thought —
and, as a separately opted-in step, garbage-collects them (not reversible)
once both restore windows have elapsed under their non-zero defaults — a
cycle count and a wall-clock duration, either of which can be configured to
0 to disable that window. OFF by default; once enabled, archived thoughts
drop out of default retrieval and can be restored (restore_thought /
include_archived).
→ See docs/memory-hygiene.md for the
loop, protection, restore windows, and the honest deletion posture.
Tamper-Evident Thought/Edge Journal
Opt-in hash-chain journal that records thought and edge mutations (plus
action status/verification_status transitions) as SHA-256-linked, before/after
entries — a tamper-evident thought/edge journal, not a whole-database audit
(embeddings and action creation are not covered). Off by default, one config flag to
enable. Query history with store.journal.get_entries(...) and validate the
chain with store.verify_journal(), which audits whatever chain is on disk
independent of the current journal.enabled state — the store.journal writer
handle does not exist while journaling is off, but the chain earlier sessions
wrote is still in journal_entry and still needs verifying.
→ See docs/audit-trail.md for enabling, querying,
verification, and the security model (what "tamper-evident" does and does not
guarantee).
Multi-Service Isolation
Run multiple independent databases under one EngravaManager:
from pathlib import Path
from engrava import EngravaManager
async with EngravaManager(data_dir=Path("./data")) as mgr:
agent_a = await mgr.get_store("agent-a")
agent_b = await mgr.get_store("agent-b")
# Completely isolated databasesMCP Server
Want Engrava as a memory server for your agent? The MCP server ships as its own
package, engrava-mcp — a native stdio server (no HTTP shim) with read
tools, optional write tools, attachable engrava:// resources, and guided
prompts, for any MCP client (Claude Desktop, Claude Code, Cursor, Windsurf,
VS Code):
uvx engrava-mcp # or: pip install engrava-mcpengrava-mcp pulls engrava in transitively, so installing it also gives you
the import engrava library. See the
engrava-mcp package for install,
client configuration, the full tool/resource/prompt reference, and read-only
mode.
uvx vs. a persistent install. uvx engrava-mcp (equivalently uv tool run engrava-mcp) installs into "an ephemeral virtual environment in the uv
cache directory" per uv's own --help text — fine for the lexical/network
profiles above, which add nothing heavier than httpx. Once you are on the
local profile (engrava-mcp[local], the same sentence-transformers +
torch download as engrava[embeddings-local]), uv tool install 'engrava-mcp[local]' is the better fit: it installs once into a persistent
environment — the same uvx engrava-mcp invocation then reuses that
installed environment instead of resolving a fresh ephemeral one — and a
later uv tool upgrade engrava-mcp re-pays only the changed packages, not
the whole dependency tree.
CLI
engrava --db mydata.db info # Database stats
engrava --db mydata.db query "FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 5"
engrava --db mydata.db snapshot -o backup.jsonl
engrava --db mydata.db restore -i backup.jsonl
engrava --db mydata.db gc # Collect ARCHIVED thoughts + their edges/embeddings/actions
engrava --db mydata.db migrate # Ensure schema is up-to-date
engrava --db mydata.db export -o portable.jsongc physically deletes ARCHIVED thoughts together with every edge touching one
on either end — including edges whose other end is still live — their embeddings
and the actions sourced from them, then reconciles the vector index by removing
every vec0 row no embedding row owns; on a vec0-indexed store where
sqlite-vec cannot be loaded — most commonly because engrava[vec] is not
installed — a pass that is about to delete stops before deleting anything and
exits 1 rather than stranding those vectors in an index nothing can then reach
them through.
engrava info's --format json output carries every field of the metrics
snapshot exposed by await store.metrics(), with the snapshot's own schema
version renamed metrics_schema_version, plus two fields the snapshot itself
does not carry, db_path and database_schema_version — see Upgrade
Guide. The default text output is a shorter summary
of that same data, not the full snapshot.
See the CLI reference for every command and option.
Architecture
SQLite with WAL mode for concurrent reads
Frozen Pydantic models — immutable domain objects
Async-first — all I/O via
aiosqliteHook-based extension — zero monkey-patching
Template method pattern — subclass
SqliteEngravaCorefor extended schemasZero external services — everything runs locally in-process
Documentation
Documentation Index — choose a path by task: learn, build, operate, extend, or migrate
Core Concepts — the mental model (thought, edge, reflection, cycle, …) — start here
The Bi-temporal Model — the optional valid-time axis: query a fact as of any instant,
invalidatewithout deletingPositioning — when Engrava is (and isn't) the right tool, and how it compares
Quick Start — 5-minute setup guide
Tutorial — build a small notes memory end to end
Recipes — copy-paste snippets for common tasks (store a turn, retrieve context, TTL, dedup, …)
Building a memory-backed agent — the end-to-end agent turn loop (ingest → retrieve → generate → consolidate)
Migrating from another memory system — concept mapping, porting calls, bulk import, and scoping/multi-tenancy
Embeddings — wiring a real embedding provider (local / OpenAI / Ollama / HuggingFace / custom)
MCP server (
engrava-mcp) — expose a store to MCP clients (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code) via the standalone server package: install, run, client config, tools/resources/promptsConfiguration — YAML config format and options
Upgrade Guide — compatibility matrix, backups, and troubleshooting
Extensions — Writing custom extensions and hooks
Observability — Metrics snapshot API
Audit Trail — Tamper-evident hash-chain journal (enabling, querying, verifying, security model)
Evidence and Conflicts — model claims, provenance, contested facts, and caller-owned clarification
Security — trust boundaries, data egress, tenant isolation, encryption posture, and journal limits
Error Handling and Recovery — decide when to retry, repair input, or replace a store
Benchmarks — release gates, versioned observed values, and LongMemEval
API Reference — Full protocol and class reference
CLI Reference — every
engravacommand and optionGlossary — quick definitions of every Engrava term
MindQL — Query language syntax and examples
Troubleshooting — symptom → cause → fix for common errors
FAQ — quick answers (LLM/keys, embeddings-optional, scale, concurrency, backups, …)
Performance & Scaling — the vector-backend switch, bulk-ingest, and dreaming cost at scale
Data Lifecycle & Retention — lifecycle states, TTL, archive-vs-delete, GDPR erasure, disk reclamation
Deployment — process model, database files on disk, containers, graceful shutdown
Concurrency — the WAL single-writer model, busy timeout, and per-service isolation
Backup & Recovery — WAL-safe backups, snapshot vs file copy, restore verification
Known Limitations — Platform notes and constraints
Development
make install # deps + dev extras + local git hooks -- from the primary checkout
make gate # fast: hooks-active + lint + format check + type check + goldens drift, ~70s
make check # lint + format check + type check + the full test suite with coverage (unchanged; no hooks-active, no goldens drift)make install also wires a commit-msg hook that lints the message of the
commit a squash merge creates — including on a local branch that never opens
a pull request, where nothing else does. It refuses to install from a linked
worktree (core.hooksPath is shared across all of them). See
CONTRIBUTING.md
for what it covers and what it does not.
License
MIT — see LICENSE for details.
docs/audit-trail.md
Audit Trail (hash-chain journal)
Engrava can record changes to your thought-graph in an append-only,
hash-linked journal — a tamper-evident thought/edge journal (not a
whole-database audit). Each entry captures one mutation — insert / update /
delete of a thought or edge, or an action status/verification_status
transition — as a before/after delta, cryptographically chained to the previous
entry with SHA-256. See What gets recorded for the exact
scope (embeddings and action creation are not covered).
Read the Security model before relying on this for compliance. The chain detects accidental corruption and naive edits, but it is a keyless chain stored in the same database file — see the boundary below.
Enabling the journal
Journaling is off by default (zero overhead when disabled — the
journal_entry table exists but is never written to). Turn it on either via
configuration or the constructor.
In engrava.yaml:
database:
path: "./engrava.db"
journal:
enabled: truefrom engrava import SqliteEngravaCore
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
assert store.journal is not None # journaling is activeOr when constructing the store directly:
import aiosqlite
from engrava import SqliteEngravaCore
# Abbreviated for brevity: a bare async-with here can let a close failure
# mask a real error. See "Graceful shutdown" in deployment.md for the safe
# explicit-close shape.
async with aiosqlite.connect("engrava.db") as conn:
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn, journal_enabled=True)
await store.ensure_schema()store.journal returns the JournalWriter when journaling is enabled, or
None when it is off — so a quick if store.journal is not None: guards any
journal-specific code.
What gets recorded
When journaling is enabled, the store records a journal entry automatically
on every mutation of a thought or edge — and on an action state-transition — you
do not call the journal yourself. The recorded mutation_type values are:
mutation_type | When |
|---|---|
INSERT_THOUGHT | create_thought() |
UPDATE_THOUGHT | update_thought() |
DELETE_THOUGHT | delete_thought() (only when a row was actually deleted) |
INSERT_EDGE | create_edge() |
UPDATE_EDGE | update_edge() |
DELETE_EDGE | delete_edge() (only when a row was actually deleted) |
UPDATE_ACTION | update_action() — only when an action's status / verification_status actually changes |
The first six values are members of the
MutationTypeenum;UPDATE_ACTIONis a storedmutation_typestring emitted byupdate_action()— the enum itself is not extended.mutation_typeis a free-text column, so verification covers the entry regardless.
Each entry's delta is a {"before": ..., "after": ...} dictionary: inserts
have before: null, deletes have after: null, and updates carry both sides.
Recorded precisely — what the chain does and does not cover. The journal covers thought and edge mutations (insert / update / delete) and action state-transitions (
update_action). It does not record: embeddings (store_embedding), action creation (create_action— only an action's later transitions are journaled, not its initial insert), or read-derived access telemetry (access_count/last_accessed_at, which is regenerable and deliberately outside the chain). So this is a tamper-evident thought/edge/action-transition journal, not a whole-database audit — verify the parts it covers, and do not assume coverage of the embedding or action-creation tables. This also matters for backups — see Backup note.
TTL expiry is recorded. cleanup_expired() (and the auto-cleanup it
triggers) goes through the same journaled paths, so expiry of a thought is
captured according to the configured TTL strategy:
archive strategy → an
UPDATE_THOUGHTentry (the thought'slifecycle_statusflips toARCHIVEDandexpires_atis cleared; the delta carries the before/after).delete strategy → a
DELETE_THOUGHTentry (after: null).
(The separate engrava gc CLI command, which physically purges already-archived
rows, now also appends its own DELETE_THOUGHT entry per row it deletes — in
the same transaction as the delete — when --config names an engrava.yaml
with journal.enabled: true. Without --config, or with journaling disabled,
nothing is journaled: gc builds its own connection and only wires a journal
writer onto it when told to. gc --expired's TTL pass is journaled the same
way cleanup_expired() above already is, through the store it builds for
that pass.)
Deleting a thought also removes its incident edges, embeddings, and actions
through database cascades (and purges its vector-index row). When deletion goes
through a journaled store path, the only entry for that operation is
DELETE_THOUGHT: cascade-removed edges do not receive DELETE_EDGE entries,
and cascade-removed embeddings, actions, and vector-index rows receive no
separate journal entry. Embedding/vector mutations and action creation/deletion
are outside journal coverage generally; only action status and verification
transitions are covered. Call delete_edge() explicitly when an individually
journaled edge deletion is required.
Below core schema 12 there is no such cascade. The ON DELETE CASCADE on
edge, embedding and action arrives with the core-12 migration.
delete_thought does not rely on it: it issues its own deletes for the thought's
rows in those three tables, in the same savepoint as the parent delete, which runs
first. See
Deletion on a database that has not been migrated
for what engrava migrate cleans up on a database that already holds dangling
embedding rows.
The JournalEntry schema
Each entry is an immutable JournalEntry:
| Field | Type | Meaning |
|---|---|---|
entry_id | str | Stable UUID for this entry |
sequence_number | int | Monotonic, gapless position in the chain (starts at 1) |
mutation_type | str | One of the MutationType values above |
target_id | str | None | The affected thought_id / edge_id |
delta | dict | {"before": {...}, "after": {...}} diff |
parent_hash | str | None | SHA-256 of the previous entry (None for the first entry) |
entry_hash | str | SHA-256 of this entry's canonical content |
created_at | str | ISO-8601 UTC timestamp |
The hash is computed over the canonical string
"{sequence_number}|{mutation_type}|{target_id}|{json(delta, sort_keys)}|{parent_hash}"
via JournalWriter.compute_hash(...) (a static method, exposed for callers who
want to recompute a hash independently).
Entry ordering and content are tamper-evident; entry timestamps are not. Read the canonical string above for what the chain covers:
sequence_numberandparent_hashbind each entry's position in the order, andmutation_type,target_id, anddeltabind what it says happened — change any of them without recomputing the affected hashes and verification fails.created_atandentry_idare not in the hash preimage, so rewriting them changes no hash: a journal whose everycreated_athas been rewritten still verifies asvalid=True. Treatcreated_atas an informative timestamp, not as evidence of when a mutation happened. This is a property of the current chain format, not a bug with a cheap fix — bringingcreated_atinto the preimage changes the expected hash of every entry ever written, so covering it is a chain-format migration or verifier-versioning decision (keeping existing journals verifiable is part of the problem, not a detail after it), never a one-line change. See Security model & guarantees.
Querying history
Use store.journal.get_entries(...) to read the trail. All filters are
optional; results are ordered by sequence_number ascending.
# Everything that ever happened to one thought:
history = await store.journal.get_entries(target_id="thought-001")
for entry in history:
print(entry.sequence_number, entry.mutation_type, entry.created_at)
# Only deletions, since a timestamp, capped:
deletions = await store.journal.get_entries(
mutation_type="DELETE_THOUGHT",
since="2026-01-01T00:00:00+00:00",
limit=500,
)| Parameter | Default | Meaning |
|---|---|---|
target_id | None | Filter by the affected entity ID |
mutation_type | None | Filter by mutation type string |
since | None | ISO-8601 lower bound on created_at (inclusive), compared by instant; a value without an offset is read as UTC, and a value that cannot be read as a UTC instant raises ValueError |
limit | 100 | Maximum entries returned |
since=is a convenience filter, not an audit boundary. It comparescreated_at >= since, andcreated_atis outside the hash preimage (as isentry_id— see the schema note above). Rewriting an entry'screated_atacross a window's lower bound can therefore remove it from or add it to that window whileverify_journal()still reportsvalid=True— the two effects compound instead of one catching the other. A rewrite that stays inside one particular window leaves membership in that window unchanged, but it still changes the timestampget_entriesreturns to the caller and may change membership under other lower bounds; because the deciding column is unprotected, nosince=result is evidence. Usesince=to narrow a report over a trusted journal; do not treat "nothing since T" as proof that nothing happened since T, nor an entry's presence in the window as proof that it happened after T. Chain-covered fields (sequence_number,parent_hash) are what a time-bounded claim has to be anchored on: record the(sequence_number, entry_hash)you saw at T and audit forward from that sequence number.
Verifying integrity
Verification walks the whole chain in order, recomputes every hash, and checks
the parent-hash linkage, returning a JournalIntegrityResult. There are three
ways to run it, all backed by the same walk:
| Entry point | Use it when |
|---|---|
store.verify_journal() | You have a store and want a one-call check. Verifies the on-disk chain even when journaling is currently disabled (see below). |
store.journal.verify_integrity() | You are already holding the JournalWriter (only available while journaling is enabled). |
engrava verify | From the shell / CI / a pre-backup hook — exit 0 = intact, 1 = broken or missing database. |
result = await store.verify_journal()
if result.valid:
print(f"Chain OK — {result.entries_checked} entries verified.")
else:
print(
f"Tampering or corruption detected at sequence "
f"{result.first_invalid_sequence}: {result.error_message}"
)| Field | Type | Meaning |
|---|---|---|
valid | bool | True if every hash and link checks out |
entries_checked | int | Number of entries verified |
first_invalid_sequence | int | None | Sequence of the first broken entry, or None |
error_message | str | None | Description of the first error, or None |
An empty journal verifies as valid=True with entries_checked=0.
What this check proves. Verification proves that every journal row still on disk has self-consistent hashed content and links to the preceding row that was walked. It detects an in-row journal mutation, a changed parent link, or a mid-chain deletion/reordering when the affected hashes have not been recomputed. It does not provide any of these separate guarantees:
Chain length / tail completeness. Removing a self-consistent suffix leaves a valid prefix, so
verify_journal()cannot know that newer entries once existed. Detect this with an externally retained high-water mark and tail anchor, such as the expected(sequence_number, entry_hash)at a checkpoint.Entry timestamps.
created_atis outside the hash preimage, so a chain whose timestamps have all been rewritten verifies exactly like an untouched one. What the walk proves is the order and the content of the entries it read, not when they were written.Live-table reconciliation. The verifier reads
journal_entry; it does not compare the current thought, edge, embedding, or action tables with the latest journal deltas. A direct edit to a live thought or edge row is outside this check unless the edit also changes a journal row or breaks the SQLite file.
Verification is independent of the current journal.enabled state. Entries
are recorded only while journaling is on, but once written they stay in the
journal_entry table. store.verify_journal() (and engrava verify) audit
whatever chain is on disk — so a database that had journaling enabled in an
earlier session and reopened with it off (store.journal is None) is still
fully auditable. (store.journal.verify_integrity() is unavailable in that case
because store.journal is None; use store.verify_journal().)
Run verification on a schedule (e.g. before each backup, during incident response, or as a periodic monitoring check) rather than only ad hoc — that is what turns the chain from a passive structure into an active control.
Verifying automatically on open
Set journal.verify_on_open: true to run the walk once, when the store is
opened through SqliteEngravaCore.from_config(...). The check runs after the
schema is ensured and raises JournalIntegrityError (a subclass of
EngravaError, carrying first_invalid_sequence and error_message) instead of
returning a store when the chain does not verify — a fail-closed startup gate.
journal:
enabled: true
verify_on_open: true # refuse to open on a broken chainfrom engrava import JournalIntegrityError, SqliteEngravaCore
try:
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
...
except JournalIntegrityError as exc:
# Startup aborted: the on-disk journal did not verify.
print(f"broken at sequence {exc.first_invalid_sequence}: {exc.error_message}")verify_on_open is independent of enabled (a recorded chain is checked
even if journaling is now off) and defaults to false. Leave it off unless
you want the gate: the walk is O(entries), so on a large journal it adds a
one-time cost to every open. For periodic rather than on-open checking, call
store.verify_journal() / engrava verify on a schedule instead.
Worked example
import aiosqlite
import sys
import uuid
from engrava import (
SqliteEngravaCore,
ThoughtRecord,
ThoughtType,
Priority,
LifecycleStatus,
)
async def _close_quietly(conn: aiosqlite.Connection) -> None:
"""Close *conn*, reporting rather than raising if the close itself fails.
``aiosqlite.Connection.__aexit__`` is an unconditional ``await
close()``, so a bare ``async with aiosqlite.connect(...)`` would let a
close failure here replace whatever the block above actually raised.
Used only from the exception path below -- the ordinary success-path
close still propagates a genuine failure normally.
"""
try:
await conn.close()
except Exception as exc: # noqa: BLE001 - deliberately broad: never replace the real error
print(f"warning: failed to close the database connection: {exc}", file=sys.stderr)
conn = await aiosqlite.connect(":memory:")
try:
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn, journal_enabled=True)
await store.ensure_schema()
note = ThoughtRecord(
thought_id=str(uuid.uuid4()),
thought_type=ThoughtType.OBSERVATION,
essence="User prefers email over phone",
content="Stated during onboarding call.",
priority=Priority.P2,
lifecycle_status=LifecycleStatus.ACTIVE,
created_cycle=0,
updated_cycle=0,
source="human",
)
await store.create_thought(note)
await store.update_thought(note.thought_id, essence="User strongly prefers email")
# Two entries were recorded automatically (INSERT_THOUGHT, UPDATE_THOUGHT).
entries = await store.journal.get_entries(target_id=note.thought_id)
assert [e.mutation_type for e in entries] == ["INSERT_THOUGHT", "UPDATE_THOUGHT"]
# The chain verifies.
result = await store.journal.verify_integrity()
assert result.valid and result.entries_checked == 2
except BaseException:
await _close_quietly(conn)
raise
else:
await conn.close()Security model & guarantees
The journal is a keyless SHA-256 integrity chain stored in the same
SQLite file it protects. verify_integrity() recomputes each entry's hash
from that entry's own stored data — there is no secret key, HMAC, signature, or
external anchor.
What it protects against (in scope): the ordering and content of the entries still on disk — both are in the hash preimage, so neither can be changed without breaking the chain unless the affected hashes are recomputed.
That preimage is the five fields joined by | with no escaping, an absent field
written as the empty string:
"{sequence_number}|{mutation_type}|{target_id}|{json.dumps(delta, sort_keys=True)}|{parent_hash}".
Nothing in the encoding marks where one field ends and the next begins, so the
fields stay distinguishable because of what they can hold, not because the format
separates them. For the entries the store emits those grammars settle it:
sequence_number is decimal digits, mutation_type is one of seven fixed
literals, parent_hash is a SHA-256 hex digest (empty on the first entry), and
delta is always a JSON object dump — whose quoting rules a caller-chosen
target_id cannot imitate, so no identifier written through the store can move a
field boundary.
That guarantee does not extend to arbitrary JournalWriter.append() calls.
JournalWriter is exported, and append() does not enforce the mutation_type
vocabulary before hashing, so a caller driving it directly can write two distinct
entries with one preimage and therefore one hash — mutation_type="INSERT_THOUGHT"
with target_id="x|" collides with mutation_type="INSERT_THOUGHT|x" and no
target_id. Exploiting it takes in-process code execution against your own store,
which is already past the boundary this chain defends; treat the binding as a
property of the store's own field grammars, not of the encoding.
A direct append() call also sits outside the store's in-process write lock
(see Concurrency) — a separate,
non-security concern worth knowing here too: every mutation the store journals
on your behalf runs the append under the same lock as the write it describes,
but JournalWriter does not hold that lock itself. Appending directly through
store.journal can still land inside another task's open transaction-deferral
window and be rolled back with it. Let the store journal its own mutations.
Verification also re-serialises the stored delta before hashing it
(json.loads in, json.dumps(..., sort_keys=True) out), so what the chain binds
is the delta's decoded value, not its stored bytes: rewriting the blob's key
order, whitespace, or escaping without changing the value it decodes to verifies
clean. Nothing the delta says can change that way — but do not read a matching
hash as proof that the stored bytes are the ones originally written.
Accidental corruption inside the retained journal rows — changed hashed content or parent linkage makes verification fail.
Naive journal tampering — someone who edits, deletes from the middle, or reorders a journal row without recomputing the affected chain: the break is detected at the first inconsistent entry.
What it does NOT protect against (out of scope):
Self-consistent tail removal. The retained prefix still verifies. An external high-water mark or tail anchor is required to prove expected length.
Entry timestamps (
created_at) andentry_id. Neither is in the hash preimage. Rewriting everycreated_atin the journal — backdating the whole trail — leaves a chain that verifies asvalid=True. A rewrite that moves an entry across aget_entries(since=...)lower bound also changes that window's contents, in either direction: downward hides the entry from the window, upward plants it inside one it did not belong to. The filter reads the same unprotected column, so a timestamp-based claim about the journal ("no deletions in the last 24 hours", or "this deletion happened during the incident") rests on data the chain does not protect. Anchor time claims on the chain-covered(sequence_number, entry_hash)pair recorded at a known point instead.Direct edits to live records. Verification does not replay the journal or reconcile thought/edge/action state against it.
A chain-aware actor with write access to the database file. Because the chain is keyless and self-contained, anyone who can write to the
.dbcan edit an entry and recompute every subsequent hash, producing a fully self-consistent chain that passesstore.verify_journal()(or the enabled writer'sstore.journal.verify_integrity()) withvalid=True. The journal is not forgery-proof against an adversary (including the agent process itself) who controls the file.
If you need genuine, multi-party tamper-evidence, treat the in-file chain as one layer and add at least one of:
Restrict write access — store the
.dbon a volume only the trusted writer process can modify (OS file permissions / ownership).Anchor the chain externally — periodically export the latest
(sequence_number, entry_hash)to an append-only / WORM store, a signed log, or another system out of the writer's control. A later integrity walk plus a match against that checkpoint adds the length/tail expectation the in-file walk does not possess.Verify on a schedule — run
store.verify_journal()from a separate monitored process so a detected mismatch raises an alert. This store-level entry point also works when journaling is currently disabled.
State this boundary plainly to stakeholders: Engrava's journal gives you integrity detection for accidental damage and unsophisticated edits, not cryptographic non-repudiation against a file-level adversary.
Backup & retention note
The logical snapshot/restore path (engrava snapshot / engrava restore)
covers the thought / edge / embedding / action tables — it does not include
the journal_entry table. A snapshot is therefore not a backup of the audit
trail. What restoring from one leaves the target's journal holding depends on
how it was restored: a fresh target starts with no journal at all, and restore --clear empties journal_entry along with the data it wipes so the chain does
not outlive the data it described. A restore without --clear merges into the
target, and if that target's journal is non-empty, a merged-in record that
collides with an existing row — on a primary key or a UNIQUE constraint — is
refused outright: the whole restore rolls back, so nothing from that snapshot
is written, not even the records that would have inserted cleanly. This is the
journalled-merge collision gate. Restoring a snapshot back into the
journalled database it came from — one thought, one matching INSERT_THOUGHT
journal entry — fails with exit code 1 and:
Error: Restore refused: snapshot line 2 collides with an existing row (matching primary key or UNIQUE constraint), and the target's journal_entry table is not empty. Replacing that row would leave the audit trail describing data this merge discarded, while 'engrava verify' kept reporting the chain as valid. Re-run with --orphan-journal-entries to allow the merge and accept that gap, or with --clear to discard the journal along with the data.The gate is conservative, not precise: it refuses any such collision once a journal exists, including one on a row the journal never described anything about — it does not try to work out which collisions are actually dangerous. It never triggers when the target's journal is empty, which is the overwhelmingly common case since journalling is opt-in and the CLI never enables it itself; an ordinary merge restore into an unjournalled target is exactly as before.
Pass --orphan-journal-entries to allow the merge anyway, accepting that the
journal may end up describing data the merge just discarded. Under that
override, the merged-in records are inserted directly and are not themselves
journalled, and a journalled row can be orphaned even when none of the
snapshot's IDs collide with anything the journal describes. Within the stock
core schema, two paths do that: an incoming edge with a fresh edge_id but
the same (from_thought_id, to_thought_id, edge_type) triple as a journalled
edge replaces it through the table's UNIQUE constraint on that triple; and
replacing a thought whose own ID does collide cascades the delete, by
foreign key, to that thought's edges, embeddings, and actions — rows whose IDs
never appeared in the snapshot. An action only has a journal entry to orphan
once it has been updated at least once: create_action writes no journal
entry, only update_action does, so a freshly created action that was never
updated cascades away with nothing stale left behind. A database that carries
an extension-installed or user-defined trigger on these tables can open
further routes that need no collision at all: the extension migration runner
executes a migration's SQL verbatim, including CREATE TRIGGER (see
Extensions), so an AFTER INSERT ON thought
trigger that deletes some other row can orphan that row's journal entry on a
restore insert that collides with nothing the target holds. In every case the
journal entries describing the earlier row stay behind unchanged; verify
still reports the chain as valid even though it no longer matches what the
database holds — confirmed against the collision above: restoring it under
--orphan-journal-entries still leaves engrava verify reporting Journal integrity OK — 1 entries verified.. restore --clear sidesteps all of this
by discarding the journal along with the data it described, rather than
letting the two disagree. To preserve the journal, back
up the database file itself (see the upgrade/backup guidance), and note that
hard-deleting an audited thought still leaves its content in the journal's
before/after delta — relevant when handling erasure requests.
See also
The Enabling the journal section above is the canonical reference for the
journal.enabledconfiguration flag; the general Configuration guide covers the rest ofengrava.yaml.API Reference — the broader public API (the journal classes
JournalWriter/JournalEntry/JournalIntegrityResultand theMutationTypeenum are documented on this page).
402 files · 357 not served here
- .commitlintrc.js (not served here)
- .cursorrules (not served here)
- .gitattributes (not served here)
- .githooks/commit-msg (not served here)
- .github/ISSUE_TEMPLATE/bug.md (not served here)
- .github/ISSUE_TEMPLATE/config.yml (not served here)
- .github/ISSUE_TEMPLATE/feature.md (not served here)
- .github/ISSUE_TEMPLATE/question.md (not served here)
- .github/copilot-instructions.md
- .github/dependabot.yml (not served here)
- .github/pull_request_template.md (not served here)
- .github/release-notes-template.md (not served here)
- .github/workflows/branch-name-guard.yml (not served here)
- .github/workflows/ci.yml (not served here)
- .github/workflows/commitlint.yml (not served here)
- .github/workflows/main-carries-the-released-tag.yml (not served here)
- .github/workflows/release-merge-discipline.yml (not served here)
- .github/workflows/release.yml (not served here)
- .github/workflows/secret-scan.yml (not served here)
- .github/workflows/smoke-gate.yml (not served here)
- .gitignore (not served here)
- .releaserc.json (not served here)
- AGENTS.md
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- BRANCHING.md
- CHANGELOG.md
- CLAUDE.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- MANIFEST.in (not served here)
- Makefile (not served here)
- README.md
- SECURITY.md
- commit-scopes.json (not served here)
- docs/api-reference.md
- docs/architecture.md
- docs/audit-trail.md
- docs/backup-and-recovery.md
- docs/benchmarks.md
- docs/bitemporal.md
- docs/cli.md
- docs/concepts.md
- docs/concurrency.md
- docs/configuration.md
- docs/data-lifecycle.md
- docs/deployment.md
- docs/dreaming.md
- docs/error-handling.md
- docs/evidence-and-conflicts.md
- docs/extension-hooks.md
- docs/extensions.md
- docs/faq.md
- docs/glossary.md
- docs/guides/agent-memory.md (not served here)
- docs/guides/embeddings.md (not served here)
- docs/guides/migrating-from-other-memory.md (not served here)
- docs/index.md
- docs/known-limitations.md
- docs/memory-hygiene.md
- docs/mindql.md
- docs/observability.md
- docs/performance.md
- docs/positioning.md
- docs/quickstart.md
- docs/recipes/index.md (not served here)
- docs/scopes.md
- docs/search.md
- docs/security.md
- docs/troubleshooting.md
- docs/tutorial.md
- docs/upgrade.md
- examples/README.md (not served here)
- examples/agent_loop.py (not served here)
- examples/config.yaml (not served here)
- examples/mcp-client-config.db-path.json (not served here)
- examples/mcp-client-config.json (not served here)
- examples/mcp-client-config.readonly.json (not served here)
- examples/mcp-client-config.vscode.json (not served here)
- examples/notes_memory.py (not served here)
- examples/profile-lexical.yaml (not served here)
- examples/profile-local.yaml (not served here)
- examples/profile-network-ollama.yaml (not served here)
- examples/quickstart.py (not served here)
- examples/simple_agent.py (not served here)
- pyproject.toml (not served here)
- release-target.json (not served here)
- scripts/backfill_content_hashes.py (not served here)
- scripts/build_upgrade_fixture.py (not served here)
- scripts/bump_pyproject_version.py (not served here)
- scripts/check_computed_version_matches_target.py (not served here)
- scripts/check_cpu_only_torch.py (not served here)
- scripts/check_hooks_active.sh (not served here)
- scripts/check_main_carries_the_released_tag.py (not served here)
- scripts/check_release_branch_merge_discipline.py (not served here)
- scripts/check_release_target_was_published.py (not served here)
- scripts/check_schema_release_gate.py (not served here)
- scripts/check_smoke_gate.py (not served here)
- scripts/install_gitleaks.sh (not served here)
- scripts/install_hooks.sh (not served here)
- scripts/rebalance_p1.py (not served here)
- scripts/reenrich_reflections_to_v2.py (not served here)
- scripts/regenerate_search_goldens.py (not served here)
- scripts/run_schema_release_gate.sh (not served here)
- scripts/run_upgrade_test.sh (not served here)
- scripts/verify_artifact_version.py (not served here)
- scripts/verify_dist_cardinality.py (not served here)
- scripts/verify_release_artifacts.sh (not served here)
- scripts/verify_release_content_scan.sh (not served here)
- scripts/verify_wheel_data.py (not served here)
- scripts/verify_wheel_execution.py (not served here)
- src/engrava/__init__.py (not served here)
- src/engrava/_composition.py (not served here)
- src/engrava/benchmarks/__init__.py (not served here)
- src/engrava/benchmarks/longmemeval/README.md (not served here)
- src/engrava/benchmarks/longmemeval/__init__.py (not served here)
- src/engrava/benchmarks/longmemeval/__main__.py (not served here)
- src/engrava/benchmarks/longmemeval/cli.py (not served here)
- src/engrava/benchmarks/longmemeval/dataset_loader.py (not served here)
- src/engrava/benchmarks/longmemeval/evaluate.py (not served here)
- src/engrava/benchmarks/longmemeval/runner.py (not served here)
- src/engrava/benchmarks/synthetic/README.md (not served here)
- src/engrava/benchmarks/synthetic/__init__.py (not served here)
- src/engrava/benchmarks/synthetic/__main__.py (not served here)
- src/engrava/benchmarks/synthetic/datasets/__init__.py (not served here)
- src/engrava/benchmarks/synthetic/datasets/synthetic-v1.json (not served here)
- src/engrava/benchmarks/synthetic/evaluate.py (not served here)
- src/engrava/benchmarks/synthetic/generate.py (not served here)
- src/engrava/benchmarks/synthetic/runner.py (not served here)
- src/engrava/benchmarks/synthetic/scenarios.py (not served here)
- src/engrava/cli/__init__.py (not served here)
- src/engrava/cli/config.py (not served here)
- src/engrava/cli/exception_reporting.py (not served here)
- src/engrava/cli/main.py (not served here)
- src/engrava/cli/memory_commands.py (not served here)
- src/engrava/cli/snapshot_records.py (not served here)
- src/engrava/cli/store_resolution.py (not served here)
- src/engrava/config.py (not served here)
- src/engrava/config_validation.py (not served here)
- src/engrava/cycle_providers.py (not served here)
- src/engrava/domain/__init__.py (not served here)
- src/engrava/domain/dreaming.py (not served here)
- src/engrava/domain/enums.py (not served here)
- src/engrava/domain/exceptions.py (not served here)
- src/engrava/domain/manifest.py (not served here)
- src/engrava/domain/models/__init__.py (not served here)
- src/engrava/domain/models/_temporal.py (not served here)
- src/engrava/domain/models/action.py (not served here)
- src/engrava/domain/models/edge.py (not served here)
- src/engrava/domain/models/embedding.py (not served here)
- src/engrava/domain/models/filters.py (not served here)
- src/engrava/domain/models/journal.py (not served here)
- src/engrava/domain/models/metrics.py (not served here)
- src/engrava/domain/models/mutation_type.py (not served here)
- src/engrava/domain/models/provenance.py (not served here)
- src/engrava/domain/models/search.py (not served here)
- src/engrava/domain/models/thought.py (not served here)
- src/engrava/domain/models/ttl.py (not served here)
- src/engrava/domain/protocols/__init__.py (not served here)
- src/engrava/domain/protocols/cycle_provider.py (not served here)
- src/engrava/domain/protocols/derived_records.py (not served here)
- src/engrava/domain/protocols/dreaming.py (not served here)
- src/engrava/domain/protocols/embedding_provider.py (not served here)
- src/engrava/domain/protocols/engrava_core.py (not served here)
- src/engrava/domain/protocols/engrava_read.py (not served here)
- src/engrava/domain/protocols/hooks.py (not served here)
- src/engrava/embeddings/__init__.py (not served here)
- src/engrava/embeddings/callback.py (not served here)
- src/engrava/embeddings/huggingface.py (not served here)
- src/engrava/embeddings/ollama.py (not served here)
- src/engrava/embeddings/openai_compatible.py (not served here)
- src/engrava/embeddings/sentence_transformer.py (not served here)
- src/engrava/extensions/__init__.py (not served here)
- src/engrava/extensions/discovery.py (not served here)
- src/engrava/extensions/dreaming.py (not served here)
- src/engrava/extensions/dreaming_cluster_quality.py (not served here)
- src/engrava/extensions/dreaming_keyphrases.py (not served here)
- src/engrava/extensions/dreaming_reflection_content.py (not served here)
- src/engrava/extensions/dreaming_reflection_extent.py (not served here)
- src/engrava/extensions/dreaming_signals.py (not served here)
- src/engrava/extensions/structural_split.py (not served here)
- src/engrava/extensions/vector_sqlite_vec.py (not served here)
- src/engrava/infrastructure/__init__.py (not served here)
- src/engrava/infrastructure/read_only_store.py (not served here)
- src/engrava/infrastructure/service_manager.py (not served here)
- src/engrava/infrastructure/sqlite/__init__.py (not served here)
- src/engrava/infrastructure/sqlite/aiosqlite_connect.py (not served here)
- src/engrava/infrastructure/sqlite/centroid.py (not served here)
- src/engrava/infrastructure/sqlite/connection_revocation.py (not served here)
- src/engrava/infrastructure/sqlite/engrava_core.py (not served here)
- src/engrava/infrastructure/sqlite/extension_migrations.py (not served here)
- src/engrava/infrastructure/sqlite/hygiene.py (not served here)
- src/engrava/infrastructure/sqlite/journal_writer.py (not served here)
- src/engrava/infrastructure/sqlite/schema_core.sql (not served here)
- src/engrava/infrastructure/sqlite/vector_sqlite_vec.py (not served here)
- src/engrava/metadata.py (not served here)
- src/engrava/mindql/__init__.py (not served here)
- src/engrava/mindql/executor.py (not served here)
- src/engrava/mindql/parser.py (not served here)
- src/engrava/py.typed (not served here)
- tests/__init__.py (not served here)
- tests/_shell_command.py (not served here)
- tests/_workflow_yaml.py (not served here)
- tests/benchmarks/__init__.py (not served here)
- tests/benchmarks/fixtures/longmemeval_tiny.json (not served here)
- tests/benchmarks/test_longmemeval_cli.py (not served here)
- tests/benchmarks/test_longmemeval_evaluate.py (not served here)
- tests/benchmarks/test_longmemeval_hygiene.py (not served here)
- tests/benchmarks/test_longmemeval_llm_mode.py (not served here)
- tests/benchmarks/test_longmemeval_loader.py (not served here)
- tests/benchmarks/test_longmemeval_runner.py (not served here)
- tests/benchmarks/test_synthetic_e2e.py (not served here)
- tests/benchmarks/test_synthetic_evaluate.py (not served here)
- tests/benchmarks/test_synthetic_frozen.py (not served here)
- tests/benchmarks/test_synthetic_generator.py (not served here)
- tests/benchmarks/test_synthetic_scenarios.py (not served here)
- tests/conftest.py (not served here)
- tests/data/bare_command_goldens.json (not served here)
- tests/docs/__init__.py (not served here)
- tests/docs/_documented_defaults_scan.py (not served here)
- tests/docs/_import_resolution.py (not served here)
- tests/docs/_md_blocks.py (not served here)
- tests/docs/test_docs_config_defaults.py (not served here)
- tests/docs/test_docs_config_examples.py (not served here)
- tests/docs/test_docs_examples_behavior.py (not served here)
- tests/docs/test_docs_examples_compile.py (not served here)
- tests/docs/test_docs_examples_coverage.py (not served here)
- tests/docs/test_docs_examples_execute.py (not served here)
- tests/docs/test_docs_examples_full_census.py (not served here)
- tests/docs/test_docs_examples_imports.py (not served here)
- tests/docs/test_docs_exceptions_table.py (not served here)
- tests/docs/test_docs_no_commit_citations.py (not served here)
- tests/docs/test_docs_shell_examples.py (not served here)
- tests/docs/test_error_handling_names_every_exception.py (not served here)
- tests/docs/test_md_blocks_fences.py (not served here)
- tests/docs/test_schema_version_error_docs_coverage.py (not served here)
- tests/domain/__init__.py (not served here)
- tests/domain/models/__init__.py (not served here)
- tests/domain/models/test_filters.py (not served here)
- tests/domain/models/test_temporal_fields.py (not served here)
- tests/domain/test_derived_records_types.py (not served here)
- tests/examples/__init__.py (not served here)
- tests/examples/_examples_census.py (not served here)
- tests/examples/test_examples_cleanup_close.py (not served here)
- tests/examples/test_examples_readme_census.py (not served here)
- tests/examples/test_mcp_client_configs.py (not served here)
- tests/examples/test_quickstart_runs.py (not served here)
- tests/extensions/__init__.py (not served here)
- tests/extensions/test_structural_split.py (not served here)
- tests/search_contract/__init__.py (not served here)
- tests/search_contract/conftest.py (not served here)
- tests/search_contract/golden_fixtures.py (not served here)
- tests/search_contract/goldens/from_config_search.yaml (not served here)
- tests/search_contract/goldens/fts_expert_normalization.json (not served here)
- tests/search_contract/goldens/hybrid_ranked_results.json (not served here)
- tests/search_contract/goldens/hybrid_ranked_results_from_config.json (not served here)
- tests/search_contract/test_archived_retrieval_exclusion.py (not served here)
- tests/search_contract/test_cross_arm_liveness.py (not served here)
- tests/search_contract/test_fts_expert_syntax_fallback.py (not served here)
- tests/search_contract/test_fts_fallback_log_content.py (not served here)
- tests/search_contract/test_fts_query_normalization_robustness.py (not served here)
- tests/search_contract/test_hybrid_weight_conservation.py (not served here)
- tests/search_contract/test_search_contract.py (not served here)
- tests/search_contract/test_search_goldens.py (not served here)
- tests/search_contract/test_vector_arm_degradation.py (not served here)
- tests/test_action_outcome_feedback.py (not served here)
- tests/test_aiosqlite_connect.py (not served here)
- tests/test_auto_embed_failure_log_content.py (not served here)
- tests/test_backfill_content_hashes.py (not served here)
- tests/test_begin_immediate_contention_is_typed.py (not served here)
- tests/test_cancellation_during_write_unit_entry.py (not served here)
- tests/test_check_cpu_only_torch.py (not served here)
- tests/test_ci_workflow_permissions.py (not served here)
- tests/test_cli.py (not served here)
- tests/test_cli_memory_verbs.py (not served here)
- tests/test_cli_memory_verbs_config.py (not served here)
- tests/test_cli_memory_verbs_embedding.py (not served here)
- tests/test_cli_memory_verbs_error_boundary.py (not served here)
- tests/test_cli_store_resolution_goldens.py (not served here)
- tests/test_cognitive_journal.py (not served here)
- tests/test_commit_scope_vocabulary.py (not served here)
- tests/test_concurrency_contract.py (not served here)
- tests/test_config.py (not served here)
- tests/test_config_validation_parity.py (not served here)
- tests/test_create_delete_journal_append_atomicity.py (not served here)
- tests/test_create_thought_performance.py (not served here)
- tests/test_cycle_provider.py (not served here)
- tests/test_de_fragmentation_collapse.py (not served here)
- tests/test_dedup_migration.py (not served here)
- tests/test_dedup_write_contention.py (not served here)
- tests/test_deletion_blast_radius.py (not served here)
- tests/test_deprecated_aliases.py (not served here)
- tests/test_derived_child_failure_transaction_isolation.py (not served here)
- tests/test_derived_records_seam.py (not served here)
- tests/test_derived_reuse_transaction_cleanup.py (not served here)
- tests/test_docs_anchor_links.py (not served here)
- tests/test_docs_no_deleted_observability_symbols.py (not served here)
- tests/test_dreaming.py (not served here)
- tests/test_dreaming_activation.py (not served here)
- tests/test_dreaming_boilerplate_filter.py (not served here)
- tests/test_dreaming_cluster_quality.py (not served here)
- tests/test_dreaming_clusters.py (not served here)
- tests/test_dreaming_content_v1_v2_compat.py (not served here)
- tests/test_dreaming_edges.py (not served here)
- tests/test_dreaming_gating_integration.py (not served here)
- tests/test_dreaming_integration.py (not served here)
- tests/test_dreaming_keyphrases.py (not served here)
- tests/test_dreaming_keyphrases_blocklist.py (not served here)
- tests/test_dreaming_lifecycle.py (not served here)
- tests/test_dreaming_metadata_filter.py (not served here)
- tests/test_dreaming_priority_bias.py (not served here)
- tests/test_dreaming_reflection_content.py (not served here)
- tests/test_dreaming_reflection_content_quality_fixes.py (not served here)
- tests/test_dreaming_reflection_extent.py (not served here)
- tests/test_edge_metadata.py (not served here)
- tests/test_edge_metadata_migration.py (not served here)
- tests/test_embedding_providers.py (not served here)
- tests/test_end_to_end_integration.py (not served here)
- tests/test_engrava_completeness.py (not served here)
- tests/test_exception_exports.py (not served here)
- tests/test_extension_infrastructure_boundary.py (not served here)
- tests/test_extension_load_with_migrations.py (not served here)
- tests/test_extension_migrations.py (not served here)
- tests/test_failed_commit_recovery.py (not served here)
- tests/test_from_config_integration.py (not served here)
- tests/test_hooks_active_gate.py (not served here)
- tests/test_hooks_wiring.py (not served here)
- tests/test_hot_path_index_migration.py (not served here)
- tests/test_hybrid_search_enhanced.py (not served here)
- tests/test_hygiene_journal_append_atomicity.py (not served here)
- tests/test_ingest_deduplication.py (not served here)
- tests/test_ingest_ergonomics.py (not served here)
- tests/test_invalidate.py (not served here)
- tests/test_journal_append_atomicity.py (not served here)
- tests/test_journal_append_recurrence_guard.py (not served here)
- tests/test_journal_verification.py (not served here)
- tests/test_main_carries_the_released_tag.py (not served here)
- tests/test_memory_hygiene.py (not served here)
- tests/test_metadata_helpers.py (not served here)
- tests/test_metadata_migration.py (not served here)
- tests/test_metrics.py (not served here)
- tests/test_metrics_measured_census.py (not served here)
- tests/test_migration_upgrade_chains.py (not served here)
- tests/test_mind_store_branches.py (not served here)
- tests/test_mind_store_core.py (not served here)
- tests/test_mindql.py (not served here)
- tests/test_outer_rollback_recovery.py (not served here)
- tests/test_partial_field_updates.py (not served here)
- tests/test_prepare_thought_for_insert_seam.py (not served here)
- tests/test_provenance_context_capture.py (not served here)
- tests/test_pyproject_urls.py (not served here)
- tests/test_quickstart_download_size_figure.py (not served here)
- tests/test_read_only_store.py (not served here)
- tests/test_rebalance_p1.py (not served here)
- tests/test_reenrich_reflections_to_v2.py (not served here)
- tests/test_referential_integrity.py (not served here)
- tests/test_reflection_centroid_staleness.py (not served here)
- tests/test_reflection_orphan_lifecycle.py (not served here)
- tests/test_release_branch_merge_discipline.py (not served here)
- tests/test_release_merge_discipline_workflow.py (not served here)
- tests/test_release_target_gate.py (not served here)
- tests/test_release_target_was_published.py (not served here)
- tests/test_remember_recall.py (not served here)
- tests/test_retrieval_microperf.py (not served here)
- tests/test_revision_guard_matrix.py (not served here)
- tests/test_schema_release_gate.py (not served here)
- tests/test_schema_version_gate.py (not served here)
- tests/test_scoped_retrieval.py (not served here)
- tests/test_search_defaults_single_source.py (not served here)
- tests/test_search_graph_weight.py (not served here)
- tests/test_search_hybrid_graph.py (not served here)
- tests/test_search_hybrid_reflection_cap.py (not served here)
- tests/test_search_top_k_floor.py (not served here)
- tests/test_service_isolation.py (not served here)
- tests/test_service_manager_cancellation_isolation.py (not served here)
- tests/test_shell_command.py (not served here)
- tests/test_smoke_gate.py (not served here)
- tests/test_snapshot_dreaming_roundtrip.py (not served here)
- tests/test_snapshot_records.py (not served here)
- tests/test_sqlite_vec.py (not served here)
- tests/test_stale_embedding_completion.py (not served here)
- tests/test_store_embedding_atomicity.py (not served here)
- tests/test_temporal_query_performance.py (not served here)
- tests/test_thought_metadata.py (not served here)
- tests/test_tier_boundary_guard.py (not served here)
- tests/test_timestamp_canonical_form.py (not served here)
- tests/test_timestamp_normalisation_migration.py (not served here)
- tests/test_timestamp_parameters.py (not served here)
- tests/test_transaction_recency.py (not served here)
- tests/test_ttl_auto_expiry.py (not served here)
- tests/test_two_process_dedup.py (not served here)
- tests/test_two_process_read_after_the_wait.py (not served here)
- tests/test_two_process_write_busy_wait.py (not served here)
- tests/test_update_readback_atomicity.py (not served here)
- tests/test_valid_interval_invariant.py (not served here)
- tests/test_valid_time_migration.py (not served here)
- tests/test_vec0_numpy_ranking_parity.py (not served here)
- tests/test_verify_wheel_execution.py (not served here)
- tests/upgrade/__init__.py (not served here)
- tests/upgrade/fixtures.py (not served here)
- tests/upgrade/test_upgrade_matrix.py (not served here)
Advantages
- Zero external service dependencies in the core write path. evidence: README.md
- Supports bi-temporal valid-time predicates for querying facts as of any instant. evidence: docs/index.md
- Provides a tamper-evident journal for auditing thought and edge mutations. evidence: README.md
Limitations
- Requires an optional embedding provider for semantic vector search functionality. evidence: docs/index.md
- The local embedding model option involves a large initial download. evidence: README.md
- Garbage collection of archived thoughts is a destructive, non-reversible operation. evidence: README.md
Security
Risk level: Low risk
This brain runs
Git hooks (1)
.githooks/commit-msg(commit-msg):runs on git commit-msg
Agent instructions that run or fetch something (4)
AGENTS.md:Run `git commit` and it fires on the patterns it checks for.CLAUDE.md:pip install -e ".[dev]"CLAUDE.md:Run a clean test suite locally before pushing;CLAUDE.md:Branch from `dev`, follow Conventional Commits, run the local quality gate before pushing.
Install scripts (1)
Makefile(make install):echo "$(BLUE)>> Installing engrava + dev dependencies$(RESET)"; $(PIP) install --upgrade pip setuptools wheel; $(PIP) install -e ".[dev]"; bash scripts/install_hooks.sh; echo "$(GREEN)>> Done. Run 'make check' to verify quality gate.$(RESET)"
- Low risk: scripts-present (.githooks/commit-msg)
- Low risk: network-instructions (CLAUDE.md:40)
- Low risk: network-instructions (pyproject.toml:98)
- Low risk: scripts-present (scripts/install_hooks.sh)
Attribution and licence
- Created by
- Sovantica
- Declared authors
- Sovantica (declared in pyproject.toml)
- Publisher
- Sovantica (Official Sovantica project)
- Source
- sovantica/engrava · commit f6a8b73
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sovantica.
Install from source
357 of its files are not served here, so there is no ZIP. sovantica/engrava at f6a8b73