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/error-handling.md
Error Handling and Recovery
This guide describes the error and persistence contracts of Engrava v0.7. It is for application code that must decide whether to fix an input, retry a remote call, reconcile a partial write, or replace a store.
The central rule is: classify the failure and establish what committed before retrying. An exception does not universally mean "nothing was written," and Engrava does not make every write idempotent.
Decision table
| Signal | State after the error | Action | Retry unchanged? |
|---|---|---|---|
ConfigError, validation ValueError, invalid filter/recency/cycle, dimension or prefix mismatch | Rejected at a validation boundary in the usual path | Correct the configuration or arguments | No |
ThoughtNotFoundError, ActionNotFoundError, SourceThoughtNotFoundError, ReferentialIntegrityError | Required data is absent | Refresh identifiers or create the missing parent deliberately | No |
DuplicateEdgeError | The requested directed, typed relationship already exists | Treat the write as idempotent success or choose a different direction/type | No |
InvalidTransitionError, ReadOnlyViolationError | Requested operation is not allowed in the current state/view | Change the requested transition or use a writable capability | No |
StaleDataError | The guarded update matched no row — another guarded write bumped the row's revision in between, or deleted the row; nothing of the rejected update was written | Re-read, recompute the intended change, then issue a new update — and handle the row being gone | No, not without re-reading |
Remote embedding timeout, network error, 408, 409, 425, 429, or selected 5xx | Depends on the operation; a single thought/update may already be committed | Let the provider exhaust its bounded retry policy, then reconcile by thought ID | Not the whole write blindly |
SQLite OperationalError containing locked or busy | The operation did not complete successfully; reconcile durable state when the write boundary is ambiguous | Reduce writer contention or increase busy_timeout; retry only an operation known to converge | Only with operation-specific proof |
WriteContentionError | The call could not get the cross-connection write lock, after busy_timeout and, for the dedup calls, the store's own bounded retries; the failed attempt wrote nothing | Retry the call; if it keeps recurring, remove the contention (see SQLite lock contention below) | Yes, the call itself |
WriteLockTimeoutError | A task could not get the store's in-process write lock within write_lock_acquire_timeout_seconds; the guarded write did not start | Find the cause first: a task spawned and awaited from inside another task's open suspend_auto_commit() window (drive writes from the one task that opened it), or a bound too small for a slow embedding provider (raise it) | Not until the cause is fixed |
DedupLockReentryError | A task re-entered the dedup window it already holds and was refused instead of blocking on itself | Fix the caller: an update_thought() override reached from upsert_by_hash()'s hit branch must not call back into a dedup entry point | No |
Rising fts_match_failure_count | Search retried with sanitized FTS syntax; the vector and other hybrid arms can still contribute | Inspect warning logs and offending queries | Engrava already retries the FTS arm once |
Rising vector_arm_degradation_count | The vector arm returned no results for an empty, zero, or non-finite query vector | Fix query embedding generation | No; the same vector degrades again |
EmbeddingGenerationError from single-item create/update | If this call owns its outermost transaction, the thought/update is committed; embedding is missing (create), or, only if the row already had one, left in place and now stale (update) — otherwise still none. Nested inside the caller's own suspend_auto_commit(), nothing is durable yet: the outermost window's exit decides, and rollback reverts to whatever existed when that outermost window opened, not just to before this call — an earlier write to the same thought inside the same window is undone too, and a thought created inside that window no longer exists at all after rollback | If this call owned its transaction: look up the thought and repair its embedding, do not recreate it. If it was nested and the outer window rolled back, do not assume based on which call you issued — look the thought up by id first: absent means nothing this window wrote for it survived, including an earlier create in the same window — create it; present means it reverted to whatever state existed before the window opened — reissue the write against that state (a blind create instead raises ValueError on the still-existing id) | Only when durable: retry embedding, not creation/update. Otherwise, look the thought up first and branch on what you find: create it if absent, or reissue the update if it is present — do not retry blindly based on which call you originally issued |
JournalIntegrityError, ExtensionMigrationError, SchemaVersionError, CoreMigrationError | Store opening or migration was rejected; CoreMigrationError leaves the schema version at the last fully applied step | Stop writes, preserve the files, diagnose or restore. For SchemaVersionError, read reason: newer_than_head needs a newer Engrava; for the two sub-floor reasons, do not delete or re-initialise the file (see below) | No |
EngravaError caught as the base class | Depends on the subclass; the base type says nothing about what committed | Handle the specific subclasses above first and put a base-class handler after them; treat anything that reaches it as unclassified | Not on the base type alone |
ConnectionQuarantinedError | The store instance is terminally unusable | Close it, create a new store over a fresh connection, then reconcile durable state | Never on the same store |
Caller, data, and configuration errors
These failures require a changed request, not backoff:
ConfigErrormeans configuration loading, provider resolution, or a bounded configuration value failed validation.ValueErroris also used by some public write boundaries, including duplicate thought IDs and invalid metadata/provenance. Pydantic may raise its own validation error while constructing records.InvalidFilterErrorandInvalidFilterPathErrorreject an invalid compiled metadata or visibility filter before query execution.RecencyModeConflictError,InvalidRecencyArgumentError, andCycleProviderErroridentify an invalid recency request or provider result.EmbeddingModelMismatchError,EmbeddingQueryPrefixMismatchError, andVectorDimensionMismatchErrorprotect the corpus from incompatible vectors. Restore the matching model/prefix/dimension or deliberately re-embed the corpus; repeating the same call cannot repair the mismatch.EmbeddingProviderContractErroridentifies a custom embedding provider that does not expose a requiredEmbeddingProviderProtocolmember — today a publicdimension. It names the provider class and the member; retrying cannot help, add the property. A provider that has the property and fails inside it does not raise this: its own exception propagates unchanged, and is classified by whatever raised it.ThoughtNotFoundError,ActionNotFoundError,SourceThoughtNotFoundError, andReferentialIntegrityErroridentify a missing target or parent.DuplicateEdgeErroridentifies an existing(from, to, type)relationship; callers can safely use it as the idempotent-create signal without parsing SQLite error text.InvalidTransitionErrorrejects an illegal lifecycle/action transition, andReadOnlyViolationErrorrejects a write throughReadOnlyEngrava.StaleDataErrormeans a guarded update matched no row: another guarded write bumped the row'srevisionbetween your read and your write, or the row was deleted in that window. The error does not say which, and nothing of the rejected update was written. It is recoverable only through a new read-modify-write cycle: re-read the record, recompute the change, and do not replay stalechangeswithout checking the newer record. Do not assume the record still exists, because a row deleted mid-call raises this too. The engine bumpsrevisionon every guarded write, so a guarded update whose read-to-write window another writer's guarded write lands in raises this error, whatever field that other write touched, instead of silently overwriting that write. Between ordinary tasks sharing one store the write lock serialises guarded writes, so it almost never fires there; the shapes it does catch are a write from a second store on the same file and a same-task nested write from a caller-owned hook — see Concurrency.WriteLockTimeoutErrorandDedupLockReentryErrorare raised instead of a hang. The first means a task waited longer thanwrite_lock_acquire_timeout_secondsfor the store's write lock; the exception alone does not say whether a task was spawned and awaited from inside another task's opensuspend_auto_commit()window (a caller bug: issue the window's writes from the one task that opened it) or the bound is too small for a slow embedding provider or large batch (raise it). The second means the same task tried to enter the dedup window it already holds, typically anupdate_thought()override reached fromupsert_by_hash()'s hit branch that calls back intocreate_thought(deduplicate=True),get_or_create(),upsert_by_hash()orbulk_store(deduplicate=True). Repeating that call unchanged cannot help; see Concurrency and Extension hooks.DerivedRecordErrormeans a producer violated a derivation gate or collided with an unrelated identity. The source thought is already durable on the automatic post-store path; some earlier derived children may also be durable.
JournalIntegrityError and ExtensionMigrationError are operator failures,
not caller mistakes. Do not keep opening the same files in a write-capable
process. Preserve the database and WAL files, inspect the reported sequence or
migration, and follow Backup & Recovery.
SchemaVersionError and CoreMigrationError are the same kind of failure,
raised by ensure_schema() and so by from_config(),
EngravaManager.get_store(), and engrava migrate. SchemaVersionError
carries current_version and a reason:
newer_than_head: the file was written by a newer Engrava.engrava migratecannot help; upgrade Engrava before opening it.populated_sub_floorandstale_shape_sub_floor: the file is stamped below the bootstrap floor and this build cannot tell which older version produced it, so it refuses instead of stamping it current. Do not delete or re-initialise it — even when no core table holds a row, it may carry data outside the core tables — preserve it and follow Backup & Recovery.
CoreMigrationError means a migration step returned without leaving the
structure it targets (a column, table, index, foreign key, or trigger) in place.
The version is not stamped past the last fully applied step, so the next
ensure_schema() retries the remaining steps rather than treating a
half-migrated file as current; repeating the open without changing anything is
not a fix. Replace the database from a backup, or use a build that matches its
schema history.
EngravaError is the base of every typed error above except ConfigError,
which derives from ValueError. It is not raised directly and carries no
guarantee about what committed, so a handler for it belongs after the specific
ones: Python runs the first except clause that matches, and a base-class
clause written first hides the specific recovery below it. A raw
sqlite3.OperationalError is not an EngravaError either.
The complete exception surface is listed in the API Reference.
Remote embedding failures
Provider behavior is deliberately provider-specific:
OpenAICompatibleProviderretries transport timeouts/network errors and HTTP408,409,425,429,500,502,503, and504. It makes at mostmax_attemptsattempts (default3), waitingbase_retry_delay_s * attempt_numberbefore each next attempt (default waits: 1 second, then 2 seconds). Other non-success statuses, such as400,401,403, and404, fail immediately.OllamaProviderandHuggingFaceProviderdo not add an Engrava retry loop. Their dependency/provider exceptions propagate.Local, callback, and third-party providers define their own failure behavior.
Once provider attempts are exhausted, auto-embedding on the thought a caller
explicitly created or updated never swallows the error. With
require_embedding=true, Engrava wraps it in EmbeddingGenerationError;
otherwise the provider's original exception is re-raised. Strict mode changes
the exception type, not the single-item commit boundary. A derived
child's own embedding failure is a separate path: under the default
on_error="log" derivation gate, it is logged and derivation continues, so
the call that created or updated the source does not see it — see
Derived records.
For a search that only failed while generating its query vector, retrying the search after a transient provider recovery does not replay a graph write. For a write with auto-embedding, inspect the persistence boundary below before doing anything else. See Embeddings for provider setup and retry controls.
Persistence boundaries
Single thought creation
Outside suspend_auto_commit(), create_thought() (and therefore remember())
commits the thought and its journal entry before auto-embedding, TTL cleanup,
on_store, and automatic derivation run. A later failure leaves the source
thought durable.
Consequences:
An embedding-provider failure leaves a newly-created thought without an embedding. It remains available to direct/FTS retrieval but is absent from vector search.
An
on_storeor derivation failure does not roll back the source. Depending on where derivation failed, earlier derived children may already be committed.Retrying
create_thought()with the same ID raisesValueError. Retryingremember()with the defaultdeduplicate=Falsecreates another random ID; withdeduplicate=True, a byte-identical existing row is reused and itsconfirmation_countis incremented instead. Neither the default call nor the deduplicated one is a repair for a failed embedding.
Use strict embedding mode when callers need the typed thought ID for repair:
from engrava import EmbeddingGenerationError
async def create_with_partial_state(store, thought):
try:
return await store.create_thought(thought), True
except EmbeddingGenerationError as exc:
persisted = await store.get_thought(exc.thought_id)
if persisted is None:
raise RuntimeError("embedding failure did not leave the expected thought") from exc
return persisted, FalseThe False result means "the thought exists but embedding repair is required,"
not "creation failed atomically."
If the application retains the provider, it can rebuild the same payload and role-aware dispatch used by auto-embedding, then upsert the missing vector:
from engrava import RoleAwareEmbeddingProvider, ThoughtNotFoundError
async def repair_embedding(store, provider, thought_id):
thought = await store.get_thought(thought_id)
if thought is None:
raise ThoughtNotFoundError(thought_id)
essence = thought.essence.strip()
content = thought.content.strip()
payload = thought.content if content.startswith(essence) else f"{thought.essence}\n{thought.content}"
if isinstance(provider, RoleAwareEmbeddingProvider):
vector = await provider.embed_document(payload)
else:
vector = await provider.embed(payload)
return await store.store_embedding(
thought_id,
vector,
model_name=provider.model_name,
)Use the same provider/model and role prefixes that built the corpus. The store's model lock rejects an incompatible model or dimension.
Thought updates
update_thought() commits the updated row and journal entry before re-embedding
when essence or content changed, provided this call owns its outermost
transaction. Nested inside the caller's own suspend_auto_commit() window,
nothing is durable yet on any path — the outermost window's exit decides,
rolling back an uncaught failure and everything it inserted. If the update
does commit and query generation then fails, the row contains the new text
but:
a thought that had no embedding remains unembedded, and stays unfindable by vector search;
a thought that had an embedding retains the old, now stale vector, and stays findable by vector search against that outdated content;
dependent REFLECTION centroids may not yet have been rebound.
Re-read the thought and repair/recompute enrichment. Do not replay the update
unchanged and assume that doing so re-embeds: an update that no longer changes
essence or content does not enter the re-embedding branch.
Bulk ingest
bulk_store() is different. Source rows, journal entries, and batch-generated
embeddings run inside one suspend_auto_commit() transaction. Called on its
own, any exception during that phase, including a provider exception or a task
cancellation, rolls the batch back and nothing from it is persisted. The
require_embedding option controls whether a failure of the provider's embed
call is wrapped as EmbeddingGenerationError; it does not change the rollback
(see Embeddings
for what that covers). The one window the rollback does not cover is a
cancellation that lands during the final commit,
described under transaction contexts below.
Nested inside a caller's own suspend_auto_commit() window, the batch shares
that outer transaction: a row error still aborts the batch's own inserts, but
only the outermost window's exit decides commit or rollback. If the caller
catches the propagated exception and the outer window then exits cleanly, the
batch's successful prefix commits along with the rest of the outer block's
work instead of being rolled back.
Automatic derivation runs only after the batch commits. A derivation failure can
therefore leave the complete source batch durable and zero or more derived
children durable. Reconcile sources by ID/content and use derive_existing() to
complete deterministic derivation rather than blindly replaying bulk_store().
Derived records
The source is durable before automatic derivation. Each derived child's row is
then committed as its own unit before its embedding and DERIVED_FROM edge are
completed. Re-running derive_existing(source_id) reuses deterministic children
and edges and fills missing enrichment, provided the producer obeys the
deterministic content contract. A failed child undoes only its own failing
step — the row and its journal entry, the edge and its journal entry, or an
embedding attempt — never an earlier child's work, and never a caller's other
pending writes when derive_existing runs inside the caller's own
suspend_auto_commit() window or a raw BEGIN. When a step's own unwind
cannot itself be trusted to have completed, it terminally quarantines the
connection; that is one of several causes, listed under
Terminal connection quarantine.
Transaction context behavior
suspend_auto_commit() groups writes on one store connection. A normal exit
commits once; any exception escaping the block, including task cancellation,
attempts a rollback of the outermost transaction. A successful rollback re-raises
the identical body exception and leaves the connection usable. If rollback also
fails, the store is quarantined before another guarded writer can enter: an
ordinary rollback error is the explicit cause of the original body exception,
while cancellation of the rollback takes precedence over the body failure.
async def store_atomically(store, first, second, link):
async with store.suspend_auto_commit():
await store.create_thought(first)
await store.create_thought(second)
await store.create_edge(link)Operational rules:
Let an error escape the
async withblock. Catching it inside and then leaving normally tells the context manager to commit.suspend_auto_commit()holds a task-reentrant lock for its whole duration: a different task's write on the same store instance now waits for the window to close instead of joining its transaction (a wait that is bounded: pastwrite_lock_acquire_timeout_secondsit raisesWriteLockTimeoutError), and nesting on the same task is supported (only the outermost call commits or rolls back). Drive the window from one task at a time regardless — a second task's write is safe from corruption, but it still simply waits, so interleaving unrelated writes through a long-running window serialises them for no benefit. See Concurrency for the full contract.Automatic on-store derivation is skipped inside a caller-held transaction. After commit, invoke
derive_existing()explicitly for sources that need it.The rollback branch catches
BaseException, soasyncio.CancelledError(andSystemExitorKeyboardInterrupt) escaping the block body rolls the outermost transaction back like any other error. The exception to that rule is a cancellation that lands during the outermost window's owncommit(): the commit runs after the body has returned, outside the rollback branch, so it may or may not have completed by the time the cancellation propagates. Treat the outcome as indeterminate: reconcile the affected IDs from a new store before repeating anything, and do not let a later commit decide it. See thesuspend_auto_commit()entry in the 0.6 to 0.7 upgrade notes for the change.
For the supported writer topology and task boundaries, see Concurrency.
SQLite lock contention
SQLite permits many readers but only one writer. Stores opened through
SqliteEngravaCore.from_config() or EngravaManager set
PRAGMA busy_timeout=5000, so a competing connection waits for up to five
seconds before surfacing database is locked. A manually supplied connection
keeps its own pragma settings.
create_thought(deduplicate=True), get_or_create(), upsert_by_hash(),
bulk_store(deduplicate=True) and the revision-guarded updates
(update_thought, restore_thought, update_edge, update_action) convert
contention that outlasts busy_timeout into WriteContentionError, carrying
operation and attempts; from bulk_store the operation reads
"create_thought", the shared dedup path's label. The failed attempt wrote
nothing, so retrying that call outright is safe. Other write
paths can still surface the raw OperationalError from the table above.
Prefer removing contention over adding a generic retry loop:
Keep write transactions short.
Serialize writes per store/process.
Partition independent writers into separate database files with
EngravaManager.Increase
busy_timeoutonly when the added tail latency is acceptable.
busy_timeout is a latency knob, not a correctness one. It decides how long a
connection waits for the file lock; it does nothing about two stores interleaving
inside one of engrava's read-modify-write operations, which is why only one store
may write a given file
(Concurrency).
If an operation is known to be read-only and repeatable, bounded retry is reasonable. This example deliberately retries journal verification, not an arbitrary write:
import asyncio
import sqlite3
async def verify_journal_with_lock_retry(store, attempts=3):
for attempt in range(1, attempts + 1):
try:
return await store.verify_journal()
except sqlite3.OperationalError as exc:
locked = "locked" in str(exc).lower() or "busy" in str(exc).lower()
if not locked or attempt == attempts:
raise
await asyncio.sleep(0.1 * attempt)
raise AssertionError("unreachable")Do not wrap every Engrava method in this helper. After an ambiguous write error, query the durable state from a healthy connection and choose a method whose repetition is known to converge.
Self-healing search and counters
Search has two intentional degradation paths:
If the primary FTS5
MATCHraisesOperationalError, Engrava incrementsfts_match_failure_count, logs a warning, sanitizes the original query into bare mode, and retries once. If that fallback also fails, the FTS arm returns no results; hybrid search can still use its other active arms.An empty, all-zero,
NaN, or infinite query vector has no cosine direction.search_similar()incrementsvector_arm_degradation_countand returns[]. A wrong-length vector is not degraded: it raisesVectorDimensionMismatchError.
The counters are cumulative per store instance and reset when a new store is constructed. Alert on growth/deltas, not merely on a non-zero lifetime value:
async def recall_with_degradation_flags(store, query):
fts_before = store.fts_match_failure_count
vector_before = store.vector_arm_degradation_count
result = await store.recall(query)
return (
result,
store.fts_match_failure_count > fts_before,
store.vector_arm_degradation_count > vector_before,
)An unavailable FTS5 index returns an empty FTS arm. Failure to load the optional
sqlite-vec extension at store setup logs a warning and falls back to exhaustive
NumPy search; it does not increment vector_arm_degradation_count. Runtime
backend failures outside the two cases above are not covered by a blanket
self-healing guarantee.
See Search and Observability for the result-level and metrics-level signals.
Terminal connection quarantine
ConnectionQuarantinedError means Engrava could not prove that a rollback or
unwind it depends on completed, or that the connection's last operation ever
reported. The old connection may have an indeterminate open transaction, so the
store revokes all new operations and replaces its internal connection with a
failing proxy. This state never clears. Any of these triggers it:
the savepoint unwind of a guarded write (
update_thought,restore_thought,update_edge,update_action, a derived child's row insert or edge insert) failed;the savepoint unwind of a
delete_thoughtfailed, including a delete made through the TTL delete strategy or hygiene garbage collection — an ordinary error such as a trigger veto can cause this, not only a cancellation, and the derived-records seam need not be involved;the outer transaction's body failed and its rollback also failed, or the compensating rollback after a failed commit could not complete;
close()gave up waiting on the physical close afterclose_timeout_seconds— see Deployment.
The call that triggered the quarantine can surface its original error (or a
cancellation that outranks it); a close() whose bound expired raises
ConnectionQuarantinedError itself. Once quarantine is installed, the next
newly-admitted public operation raises ConnectionQuarantinedError. This
includes a writer already queued on the failed window's lock: it cannot commit
that window's abandoned writes. Retirement does not wait for physical close,
which may fail or remain pending; a concurrent owned close() shares that same
physical-close task under its existing bound. A separately retained caller-owned
raw connection is outside the core admission guarantee.
Close the old store, open a new one, and inspect durable state before deciding what to repeat:
from engrava import ConnectionQuarantinedError, SqliteEngravaCore
async def replace_and_reconcile(store, config_path, source_id):
try:
source = await store.get_thought(source_id)
except ConnectionQuarantinedError:
await store.close()
store = await SqliteEngravaCore.from_config(config_path)
source = await store.get_thought(source_id)
if source is not None:
await store.derive_existing(source.thought_id)
return storeThe caller owns the returned replacement and should close it during normal shutdown.
Do not retry against the quarantined instance, including through its journal. Quarantine detaches the real connection and schedules its best-effort close even when the store was built with the manual constructor. Any external reference to that same connection must still be treated as revoked and discarded.
Idempotency and convergence
Use these contracts narrowly:
| Operation | Repetition contract |
|---|---|
invalidate_thought(id, same_valid_until) | Idempotent; converges to the same valid-time boundary |
invalidate_edge(id, same_valid_until) | Idempotent; converges to the same valid-time boundary |
derive_existing(source_id) | Idempotent only under the deterministic producer contract; reuses children/edges and completes missing enrichment |
upsert_by_hash(same_desired_record) | State-convergent on the mutable fields, but a first-call post-commit enrichment failure still needs explicit reconciliation/derivation |
store_embedding(thought_id, same_vector, same_model) with the default deterministic embedding ID | Upserts the same vector, but refreshes embedding metadata such as created_at; convergent, not byte-identical |
create_thought() | Not idempotent; the same ID raises and a new ID creates another row |
remember() | Not idempotent; each call creates a fresh ID unless content deduplication is requested |
create_thought(deduplicate=True) / get_or_create() | Prevent duplicate content rows, but each hit increments confirmation_count; not observationally idempotent |
bulk_store() | Atomic through source+embedding commit when it owns that transaction outright — nested inside a caller's own suspend_auto_commit(), a caught row error can still commit the batch's successful prefix, while a caught embedding failure (see API reference) commits every row the batch inserted instead, since that failure only fires after every row is already in — and, either way, not safe to replay blindly after a post-commit derivation failure |
| Retrieval calls | Do not mutate graph content, but can buffer access-frequency events when access tracking is enabled |
When an API is not listed as idempotent, assume that retry requires an application-level operation key and a read-back/reconciliation step.
Related documentation
API Reference — signatures, return types, and typed errors
Troubleshooting — symptom-to-fix entries
Concurrency — WAL, busy timeout, and writer topology
Embeddings — providers, model locks, and retry settings
Observability — metrics and degradation signals
Backup & Recovery — WAL-safe backup and restore
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