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/api-reference.md
API Reference
Complete reference for engrava's public API.
Core Store
SqliteEngravaCore
The main persistence engine. All operations are async.
SqliteEngravaCore wraps an already-open aiosqlite.Connection. For a
config-driven, one-call setup that opens and owns the connection, use the
from_config factory instead.
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(":memory:") as conn:
conn.row_factory = aiosqlite.Row
store = SqliteEngravaCore(conn)
await store.ensure_schema()
# Or, config-driven (opens and owns the connection):
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
... # schema already applied by from_configConstructor
| Parameter | Type | Default | Description |
|---|---|---|---|
db | aiosqlite.Connection | — | An already-open aiosqlite connection (set row_factory = aiosqlite.Row) |
hooks | EngravaHooksProtocol | None | None | Extension hooks (defaults to DefaultEngravaHooks) |
embedding_provider | EmbeddingProviderProtocol | None | None | Provider used when auto_embed=True |
auto_embed | bool | False | Auto-embed thoughts on create/update |
require_embedding | bool | False | When True, a failure of the provider's embed call raises EmbeddingGenerationError instead of only logging a WARNING and re-raising the provider error (see Embeddings for what that covers). Two independent questions decide the outcome for the thought row. If the call does not own the 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, so a caller that catches the failure and exits that window cleanly still commits every row inserted so far, single item or batch alike, while an uncaught failure rolls it back (see bulk_store below). If the call does own the outermost transaction, the path decides: create_thought/update_thought have already committed by the time embedding runs, so the failure cannot undo them; a standalone bulk_store instead wraps its whole batch's inserts and trailing embed call in one transaction, so a failure there rolls the whole batch back regardless, and none of it is persisted (batch auto-embed is one provider call made after every row is inserted, not a per-row step, so nothing is aborted partway). What is left behind is path-specific and only meaningful for whatever actually committed: create_thought leaves no embedding row at all; update_thought leaves any embedding the row already had in place — if the update committed, that embedding is now stale against the new content, and the row is still findable by vector search against that outdated vector; if the row had no embedding before, it still has none, and remains unfindable by vector search; if the update instead rolled back — only possible when this call is nested inside a caller's own window and the caller lets the failure escape it — the durable state reverts to whatever existed when that outermost window opened, not merely to what this call itself started from: an earlier write to the same thought inside the same window is undone right along with it; if the thought was created inside that same window, it no longer exists at all afterward; if it already existed before the window opened, it reverts to that pre-window state, and the retained embedding matches it only if it already did — an earlier update on the same thought, before this window ever opened, whose own re-embed failed can already have left that pre-window state stale, and this rollback neither detects nor repairs that. No effect unless auto_embed=True. |
search_config | SearchConfig | None | None | Default hybrid-search weights, recency half-lives, graph expansion, reflection handling, and bounded candidate-pool settings |
journal_enabled | bool | False | Record mutations in the hash-chain journal |
ttl_strategy | str | "archive" | Expiry action: "archive" or "delete" |
ttl_check_every_n | int | 0 | Automatic expiry-cleanup cadence, counted in thought create/update calls, not every store operation; 0 disables automatic cleanup |
ttl_default_seconds | int | None | None | Default relative TTL for new thoughts |
metrics_config | MetricsConfig | None | None | Metrics enablement and latency-window settings |
manifests | Sequence[ExtensionManifest] | () | Extension manifests whose schema migrations are applied by ensure_schema() |
access_tracking_enabled | bool | False | Buffer retrieval access events for the dreaming frequency signal |
hygiene_policy | HygienePolicyConfig | None | None | Memory Hygiene policy; None means run_hygiene() is unavailable |
derive_gates | DeriveGates | None | None | Automatic derived-record production gates; defaults to disabled gates |
cycle_provider | CycleProvider | None | None | Runtime-only cognitive-cycle source used when a supported call omits current_cycle and no explicit recency_now selects transaction-time recency; explicit cycle values, including 0, win |
write_lock_acquire_timeout_seconds | float | 600.0 | Bound on how long a different task waits to acquire the in-process guarded-write lock before WriteLockTimeoutError is raised instead of hanging; the same task re-enters its own lock for free regardless of this value. Raise it for a store whose embedding provider or batch sizes can legitimately hold the lock (e.g. bulk_store's batch embedding call) longer than the default covers |
The
SqliteEngravaCore(db_path=...)form does not exist — pass a connection, or useawait SqliteEngravaCore.from_config(path).
Factory
| Method | Returns | Description |
|---|---|---|
await SqliteEngravaCore.from_config(config_path, *, cycle_provider=None, write_lock_acquire_timeout_seconds=600.0) | SqliteEngravaCore | Build from a YAML config; opens + owns the connection and applies the schema. cycle_provider is a runtime object and is never read from YAML. write_lock_acquire_timeout_seconds is forwarded verbatim to the constructor (below) and is likewise never read from YAML — a runtime tuning knob, not corpus-affecting configuration. Use the result as an async context manager. |
Schema
| Method | Description |
|---|---|
await ensure_schema() | Create tables if missing, run migrations |
await verify_journal() | Verify the complete persisted hash chain and return JournalIntegrityResult |
await close() | Close the database connection |
Thought CRUD
create_thought takes a single frozen ThoughtRecord object (build it, then
pass it) and returns the persisted record — it does not take field
keyword arguments and does not return a UUID string.
| Method | Returns | Description |
|---|---|---|
await remember(text, *, metadata=None, deduplicate=False) | ThoughtRecord | Store a bare string as an ACTIVE P3 NOTE at cycle 0; its first 200 characters become essence. Use an explicit ThoughtRecord when the caller owns cognitive cycles or needs other fields. |
await create_thought(thought, *, expires_after_seconds=None, deduplicate=False) | ThoughtRecord | Persist a ThoughtRecord; returns the stored record. Raises ValueError if the ID already exists. |
await get_or_create(thought, *, expires_after_seconds=None) | tuple[ThoughtRecord, bool] | Content-hash convenience over dedup: returns (existing, False) on a hash hit (confirmation bumped, like deduplicate=True) or (new, True) on a miss. The bool tells you whether it created, removing the check-then-create round trip. Does not alter a matched row's fields. |
await upsert_by_hash(thought, *, expires_after_seconds=None) | ThoughtRecord | Update-on-match upsert: on a hash hit, overwrites the stored row's mutable fields (essence, priority, metadata, visibility, lifecycle_status, source, confidence, source_type, thought_type) from thought and returns it (no confirmation bump — distinct from deduplicate=True, which returns the stored record unchanged); on a miss, inserts. content is never rewritten (it is the hash key). |
await bulk_store(thoughts, *, deduplicate=False) | list[ThoughtRecord] | Transactional batch insert: the whole list commits once. Called on its own, any row error rolls the whole batch back and nothing is persisted. Nested inside the caller's own suspend_auto_commit() window, a row error still aborts the batch's inserts, but only the outermost window's exit decides commit/rollback — if the caller catches the error and that outer window then exits cleanly, the batch's successful prefix commits along with the rest of the window's work. Order preserved. Under auto_embed, all thoughts are embedded in one batch provider call. deduplicate applies per row. |
await get_thought(thought_id) | ThoughtRecord | None | Retrieve by ID; None if not found |
await update_thought(thought_id, **changes) | ThoughtRecord | Partial update: writes only the fields named in changes (plus the updated_at stamp), so columns another writer changed meanwhile are preserved; returns the row read back after the write. Raises ThoughtNotFoundError if the thought is missing when the call starts. Raises StaleDataError when the guarded write matches no row — any other guarded write bumped the row's revision in between (which rejects this update whatever field it touched), or deleted the row — see Concurrency |
await restore_thought(thought_id, *, current_cycle=None) | ThoughtRecord | Un-archive: transition an ARCHIVED thought back to ACTIVE, clearing both hygiene markers (archived_at_cycle and archived_at). It does not restore expires_at: an expiry cleared during hygiene archival stays cleared after restore. The reversible counterpart to the memory-hygiene / TTL / manual archive paths, journaled as an UPDATE_THOUGHT. Raises ThoughtNotFoundError if missing, InvalidTransitionError if the thought is not currently ARCHIVED, StaleDataError if the guarded write matches no row — another guarded write bumped revision in between, or deleted the row (see update_thought for what that guard does and does not catch). This is the canonical un-archive path; a raw update_thought(lifecycle_status=...) back to ACTIVE does not manage those markers. |
await list_thoughts(...) | list[ThoughtRecord] | List with filters (keyword-only) |
await count_thoughts(...) | int | Count with filters (keyword-only) |
await delete_thought(thought_id) | bool | Hard delete; True if a row was removed. Deleting a thought also deletes the edges for which it is either endpoint, its embedding, and its linked actions — delete_thought issues those deletes itself, in the same savepoint as the parent delete, rather than relying on the ON DELETE CASCADE the core-12 migration adds to edge, embedding and action. See Deletion on a database that has not been migrated for a database that already holds dangling embedding rows from an older engrava build. |
await invalidate_thought(thought_id, valid_until) | ThoughtRecord | Close the thought's valid-time interval at the given ISO-8601 instant — deterministic, idempotent, non-cascading, and not a delete (the row stays on file and remains retrievable for instants before valid_until). Raises ThoughtNotFoundError if missing. See Bi-temporal Model |
await record_access(thought_id) | None | Mark a thought as accessed — bumps access_count and sets last_accessed_at; raises ThoughtNotFoundError if missing. Drives the access-frequency dreaming signal. |
import uuid
from engrava import ThoughtRecord, ThoughtType, Priority, LifecycleStatus
record = ThoughtRecord(
thought_id=str(uuid.uuid4()),
thought_type=ThoughtType.OBSERVATION,
essence="Short summary (1-200 chars)",
content="Full text content.",
priority=Priority.P2,
lifecycle_status=LifecycleStatus.ACTIVE,
created_cycle=0,
updated_cycle=0,
source="human",
)
stored = await store.create_thought(record)from engrava import LifecycleStatus
# Archive, then restore — a lossless round-trip.
await store.update_thought(stored.thought_id, lifecycle_status=LifecycleStatus.ARCHIVED)
restored = await store.restore_thought(stored.thought_id, current_cycle=42)
assert restored.lifecycle_status is LifecycleStatus.ACTIVEcreate_thought keyword-only options
| Parameter | Type | Default | Description |
|---|---|---|---|
expires_after_seconds | int | None | None | Relative TTL; overrides the store default |
deduplicate | bool | False | Collapse identical content (SHA-256) into the existing thought, bumping confirmation_count |
list_thoughts / count_thoughts filters (keyword-only)
| Parameter | Type | Description |
|---|---|---|
thought_type | str | None | Filter by type |
lifecycle_status | str | None | Filter by status |
priority | str | None | Filter by priority |
include_expired | bool | Include expired thoughts (default False) |
min_cycle | int | None | Minimum updated_cycle, inclusive (list_thoughts only) |
max_cycle | int | None | Maximum updated_cycle, inclusive (list_thoughts only) |
limit | int | Max results (list_thoughts only; default 50) |
offset | int | Results to skip (list_thoughts only; default 0) |
list_thoughtsalso supportsvisibility,exclude_visibility, andprovenance_filter(see Provenance capture).
Provenance capture
Attach optional, typed write-time provenance to a thought — the signals that
become irrecoverable once a synthesised thought exists (which session/actor
produced it, what query and instruction shaped it, which thoughts it was built
from). Set ThoughtRecord.provenance to a ProvenanceContext before
create_thought; every field is optional and bounded, and the default None is
byte-identical to not using provenance at all.
from engrava.domain.models.provenance import ProvenanceContext
record = ThoughtRecord(
# ... the usual fields ...
provenance=ProvenanceContext(
session_id="sess-42", # indexed identity hint
actor_id="agent-a", # indexed identity hint
retrieval_query="remote work trade-offs", # ≤4096 chars
instruction_context="summarise for a busy exec", # ≤4096 chars
retrieval_context_ids=["t-1", "t-2"], # ≤128 ids, ≤256 chars each
),
)
await store.create_thought(record)ProvenanceContext field | Type | Notes |
|---|---|---|
session_id | str | None | Session handle. Indexed for lookup. ≤256 chars |
actor_id | str | None | Actor/agent handle. Indexed. ≤256 chars |
retrieval_query | str | None | Query text that retrieved the feeding context. ≤4096 chars |
instruction_context | str | None | Instruction / system-prompt fragment. ≤4096 chars |
retrieval_context_ids | list[str] | None | Source thought ids. Queryable, not indexed. ≤128 × ≤256 chars |
Query by provenance. list_thoughts(provenance_filter=...) takes a
MetadataFilter — an AND of FieldPredicates over the provenance JSON
column. Predicates on $.session_id / $.actor_id use the provenance identity
index; rows whose provenance is NULL or malformed JSON never match a
non-empty filter.
from engrava.domain.models.filters import FieldOp, FieldPredicate, MetadataFilter
mine = await store.list_thoughts(
provenance_filter=MetadataFilter(
[FieldPredicate("$.session_id", FieldOp.EQ, "sess-42")]
),
)Untrusted hint — never identity, authentication, or authorization. Provenance is descriptive-only: the engine grants it zero authority and consults it for no access, ranking, or consolidation decision.
actor_idis not a tenant boundary — tenant isolation is the store's file boundary (one store per tenant). The engine never infers provenance; the caller passes it explicitly.
Edge CRUD
create_edge takes a single EdgeRecord object and returns the persisted
record. It raises ReferentialIntegrityError when an endpoint thought does
not exist and DuplicateEdgeError when the same directed, typed relationship
already exists.
| Method | Returns | Description |
|---|---|---|
await create_edge(edge) | EdgeRecord | Persist an EdgeRecord; raises ReferentialIntegrityError on a missing endpoint or DuplicateEdgeError for an existing (from, to, type) relationship |
await get_edges(thought_id, *, direction='BOTH', limit=None) | list[EdgeRecord] | Edges for a thought (direction is 'IN'/'OUT'/'BOTH', keyword-only); limit must be None or an integer >= 0 and keeps the highest-weight edges, 0 returns an empty list, None returns every edge unordered. Raises ValueError if limit is negative or a bool |
await list_edges(*, edge_type=None, source=None, filters=None, limit=5000) | list[EdgeRecord] | List edges with optional filters (filters is a typed MetadataFilter over the edge metadata; see the metadata field note) |
await update_edge(edge_id, **changes) | EdgeRecord | Update edge fields — only those that actually change (a no-op writes nothing). Raises ValueError if the edge does not exist at the initial read. Raises StaleDataError on a real change whose guarded write matches no row — another guarded write bumped revision in between, or deleted the row — see Concurrency |
await delete_edge(edge_id) | bool | Hard delete; True if a row was removed |
await invalidate_edge(edge_id, valid_until) | EdgeRecord | Close the edge's valid-time interval at the given ISO-8601 instant — deterministic, idempotent, and not a delete (the row stays on file). Invalidating a thought does not cascade to its edges; invalidate them separately. See Bi-temporal Model |
The database permits at most one edge for each
(from_thought_id, to_thought_id, edge_type) tuple, independently of
edge_id; attempting to insert the same directed, typed relationship twice
raises DuplicateEdgeError. Reversing the endpoints or using a different edge
type produces a distinct edge. A duplicate caller-supplied edge_id for a
different relationship remains a separate integrity failure. A hard thought
delete cascades to edges where
the thought is either endpoint; invalidate_thought() does not remove or
invalidate any edge.
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.
import uuid
from engrava import EdgeRecord, EdgeType
await store.create_edge(
EdgeRecord(
edge_id=str(uuid.uuid4()),
from_thought_id=src_id,
to_thought_id=dst_id,
edge_type=EdgeType.ASSOCIATED,
weight=0.8,
created_cycle=0,
)
)REFLECTION lineage
Helpers for navigating the CONSOLIDATED_FROM graph that dreaming builds
between a REFLECTION and the source thoughts it summarises.
| Method | Returns | Description |
|---|---|---|
await consolidated_member_ids(reflection_id) | list[str] | The thought IDs a REFLECTION was consolidated from |
await consolidated_source_statuses(reflection_id) | list[str] | The lifecycle statuses of those source thoughts (e.g. to detect a fully-archived, orphaned cluster) |
await reflections_consolidated_from(source_id) | list[str] | The REFLECTION IDs that consolidated a given source thought (the reverse direction) |
await thought_exists_by_source(*, source, thought_type_value) | bool | Whether any thought exists with the given source and type — keyword-only |
# Walk a REFLECTION down to its sources, and back from a source to its REFLECTIONs.
member_ids = await store.consolidated_member_ids(reflection_id)
for thought_id in member_ids:
source = await store.get_thought(thought_id)
if source is not None:
print(source.essence)
# Detect an orphaned cluster — every source archived/gone:
statuses = await store.consolidated_source_statuses(reflection_id)
is_orphaned = bool(statuses) and all(s != "ACTIVE" for s in statuses)
# Reverse direction: which REFLECTIONs summarise this source?
parents = await store.reflections_consolidated_from(member_ids[0])
# Exact-source existence check (e.g. dreaming's idempotency guard — a REFLECTION's
# source is "dreaming:<cluster_hash>", so match the full value, not a prefix):
exists = await store.thought_exists_by_source(
source="dreaming:abc123def4567890", thought_type_value="REFLECTION"
)Embedding Operations
| Method | Returns | Description |
|---|---|---|
await store_embedding(thought_id, vector, *, model_name="all-MiniLM-L12-v2", embedding_id=None) | EmbeddingRecord | Store an embedding vector (dimension derived from len(vector)) |
await get_embedding(thought_id) | EmbeddingRecord | None | Retrieve embedding |
await search_similar(query_vector, top_k=10, threshold=0.0, *, include_archived=False) | list[tuple[str, float]] | Cosine similarity search → (thought_id, score). Raises VectorDimensionMismatchError on a wrong-length query vector; a degenerate vector degrades to [] (see Known Limitations). |
Full-Text & Hybrid Search
search_fts and search_similar return (thought_id, score) tuples — fetch
the record with get_thought when you need its fields. search_hybrid
returns a single HybridSearchResult container.
| Method | Returns | Description |
|---|---|---|
await recall(query, *, top_k=10, current_cycle=None, recency_now=None, recency_now_half_life=None, filters=None, visibility=None, collapse_key=None, collapse_max_per_unit=None, include_archived=False) | HybridSearchResult | Ergonomic shorthand over search_hybrid for the common retrieval case; supports either recency axis, scoped retrieval, collapse, and archived-row opt-in |
await search_fts(query, top_k=10, *, include_archived=False) | list[tuple[str, float]] | FTS5/BM25 text search → (thought_id, bm25_score); malformed expert expressions retry once through safe bare normalization |
await search_hybrid(query_text, query_vector=None, *, top_k=10, ...) | HybridSearchResult | Combined FTS + vector + recency + priority + graph |
await search_reflections_only(query_text, query_vector=None, *, top_k=10, current_cycle=None) | HybridSearchResult | Search restricted to active, unexpired REFLECTION thoughts, ranked by vector similarity and optional cycle recency. Expiry is evaluated against one UTC instant captured for the call; expires_at <= now is excluded. |
The complete search_hybrid signature is:
async def search_hybrid(
query_text: str,
query_vector: list[float] | None = None,
*,
top_k: int = 10,
fts_weight: float | None = None,
vector_weight: float | None = None,
recency_weight: float | None = None,
recency_half_life: int | None = None,
current_cycle: int | None = None,
recency_now: str | None = None,
recency_now_half_life: int | None = None,
fts_top_k: int = 50,
vector_top_k: int = 50,
priority_weight: float | None = None,
graph_weight: float | None = None,
graph_edge_decay: float | None = None,
include_reflections: bool = True,
reflection_boost: float | None = None,
filters: MetadataFilter | None = None,
visibility: VisibilityQueryFilter | None = None,
collapse_key: str | Sequence[str] | None = None,
collapse_max_per_unit: int | None = None,
include_archived: bool = False,
) -> HybridSearchResult: ...current_cycle and recency_now are mutually exclusive when both are supplied
explicitly. recency_now_half_life is measured in seconds and is only valid
with recency_now; recency_half_life is measured in cognitive cycles.
Archived thoughts are excluded by default.
recall,search_fts,search_hybrid, andsearch_similardropARCHIVEDthoughts from the default candidate set (the mechanism behind Forgetting). Passinclude_archived=Trueto re-admit them for one call, orrestore_thoughtto re-activate a row.search_reflections_onlyremains restricted to active reflections and has no archive opt-in. See Search.
Scoped & de-fragmented retrieval (keyword-only)
Both recall and search_hybrid accept the same four opt-in keywords for
narrowing and de-duplicating ranked results. Every one defaults to None and
the None path is byte-identical to an unscoped query, so existing callers see
no change.
| Parameter | Type | Description |
|---|---|---|
filters | MetadataFilter | None | Metadata scope — an AND of typed FieldPredicates over the thought's metadata, applied in-arm before each arm's candidate limit so it never starves top_k. None (or an empty filter) leaves the candidate set unchanged. A query refinement, not a security boundary. |
visibility | VisibilityQueryFilter | None | Bounded (visibility IN … [OR owner = …]) refinement over $.visibility / $.owner — the "public-or-mine" pattern. A query filter, not access control: it performs no authentication, authorization, or ownership enforcement; the caller can forge owner; it is bypassable and must not be used to protect tenant data (use one store per tenant for isolation). |
collapse_key | str | Sequence[str] | None | Opt-in de-fragmentation unit key — a single metadata path ("$.session_id") or an ordered sequence forming a composite key. When set, only the single best-ranked row per caller-defined unit reaches the result and the freed slots backfill deeper distinct units. A presentation / de-dup convenience, not a filter and not isolation. The collapse step mutates no score, but setting collapse_key widens the internal candidate pool, which can rescale the min-max-normalized keyword scores and shift order among units; only collapse_key=None leaves the path byte-identical. A missing/malformed key ⇒ the row is its own unit, never collapsed. |
collapse_max_per_unit | int | None | Opt-in intra-unit retention depth for collapse_key. None (default) keeps one best row per unit; an integer >= 1 keeps up to that many highest-ranked members of a unit and lets the freed slots backfill deeper distinct units. Only effective together with collapse_key; a value < 1 is rejected. |
from engrava import MetadataFilter, VisibilityQueryFilter
from engrava import FieldOp, FieldPredicate
# Scope recall to one session's memories, keep only "public" or my own rows,
# and collapse per-turn fragments to one best row each.
result = await store.recall(
"remote work trade-offs",
top_k=10,
filters=MetadataFilter([FieldPredicate("$.session_id", FieldOp.EQ, "sess-42")]),
visibility=VisibilityQueryFilter(frozenset({"public"}), owner="alice"),
collapse_key="$.session_turn",
collapse_max_per_unit=2, # keep up to 2 rows per turn instead of 1
)Metrics & maintenance
| Method | Returns | Description |
|---|---|---|
await metrics() | EngravaMetrics | Snapshot of thought/edge counts, storage, and search-latency percentiles (see Observability) |
await max_cycle() | int | Cognitive-cycle high-water mark across thought.updated_cycle and edge.created_cycle; returns 0 for an empty store |
await cleanup_expired(now=None, *, exclude_id=None) | CleanupResult | Archive or delete thoughts past their expires_at |
await run_hygiene(*, current_cycle=None, now=None) | HygieneResult | Run one Forgetting / Memory Hygiene pass: archive cold thoughts (reversibly) and, when auto_gc_enabled, GC ones past both restore windows. Raises RuntimeError if no policy was supplied; returns an empty result when the supplied policy is disabled. |
await derive_existing(thought_id) | DeriveResult | Backfill: run the registered derived-records producer over an already-stored source thought (idempotent). Raises SourceThoughtNotFoundError if the id is absent. See Backfilling existing thoughts. |
await flush_access_buffer() | int | Persist buffered retrieval-access deltas in one batch; returns the number of buffered entries drained (distinct thought ids submitted to the update), not necessarily the number of rows actually updated — an id whose thought was deleted since it was buffered is drained but matches no row |
await retire_orphan_reflections() | int | Archive active REFLECTIONs whose entire CONSOLIDATED_FROM source set is no longer active |
attach_dreaming_extension(extension) | None | Wire a DreamingConsolidatorProtocol implementation onto the store so consolidate() can run it — the supported alternative to setting the private _dreaming_extension attribute directly. A second call replaces whatever was attached before; there is no way to detach through this method. Checks extension against the runtime_checkable DreamingConsolidatorProtocol (which looks for a run_consolidation member) and raises TypeError if that check fails; it does not check the method's signature or that it is a coroutine function, so a same-named but wrong-shaped one (sync, or a different signature) fails only once consolidate() calls it. See Dreaming. |
await consolidate(*, current_cycle=None) | ConsolidationResult | Run the dreaming extension wired by from_config; raises RuntimeError when dreaming is not enabled and uses cycle_provider when no explicit cycle is supplied |
await verify_embedding_model() | None | Raise EmbeddingModelMismatchError if the stored model lock disagrees with the configured provider |
async with store.suppress_access_tracking(): | context manager | Suppress implicit retrieval-access buffering inside the current async task; nestable and concurrency-safe, with no effect when access tracking is disabled. Used by internal maintenance and read-only views so their reads do not inflate the frequency signal. |
async with store.suspend_auto_commit(): | context manager | Defer per-call commits so a block of writes commits once; an escaping error attempts rollback. A failed rollback quarantines this store before another writer can enter, preserving the body exception with the rollback error as cause unless the rollback is cancelled. See Transaction context behavior. Holds a task-reentrant lock for its duration: a different task's write now waits for the window to close rather than joining its transaction; the same task's own nested writes (and nested windows) proceed normally. See Concurrency |
await close() | None | Close the owned connection (only when the store opened it via from_config) |
Read-only health counters (plain properties, not await, not in the metrics
snapshot): store.fts_match_failure_count and store.vector_arm_degradation_count
are monotonic int counters that surface silent, self-healing search-arm
degradations. See Observability signals.
# Bulk ingest: one transaction instead of one commit per write.
async with store.suspend_auto_commit():
for record in many_records:
await store.create_thought(record)
# commit happens once on clean exit; any exception rolls the whole block backReadOnlyEngrava
A composition wrapper that delegates reads to the wrapped store and raises
ReadOnlyViolationError on any write. Use it to hand a retrieval-only view of
shared memory to a component that should never mutate it — e.g. a sub-agent or
worker whose job is only to look things up. Delegated reads suppress access
tracking, so they do not stage deferred access-count mutations.
The guarantee is behavioural, not structural. ReadOnlyEngrava implements
the narrower EngravaReadProtocol, but also exposes signature-compatible write
methods whose only behaviour is to raise. Because EngravaCoreProtocol is a
runtime-checkable structural protocol, those blockers mean
isinstance(ro, EngravaCoreProtocol) is also True. Use
EngravaReadProtocol to type a read capability; do not use the structural core
check as evidence that a value is writable.
from engrava import ReadOnlyEngrava
ro = ReadOnlyEngrava(store)
thought = await ro.get_thought("abc") # OK
await ro.create_thought(...) # Raises ReadOnlyViolationErrorEngravaManager
Multi-service database isolation. Each service owns a separate
<data_dir>/<service_name>.db file, with its own schema, connection, embedding
configuration, and journal. Service names accepted by get_store(),
service_exists(), and delete_service() must match
^[a-z][a-z0-9_-]{0,62}$: start with a lowercase
letter, then use lowercase letters, digits, _, or -, up to 63 characters.
| Method | Description |
|---|---|
await get_store(service_name, *, migrate=True) | Lazily create/open and initialize a service store on first access. The manager caches the store, so later calls for the same name return the same live instance until close_all() or deletion (migrate is ignored on a cache hit — the schema state was already decided by whichever call created the cached instance). With the default migrate=True, construction calls ensure_schema(): a fresh service is bootstrapped and an existing behind one is brought current — this can raise SchemaVersionError. Pass migrate=False to skip ensure_schema() and open the core schema exactly as stored; for a service that does not exist yet this leaves it with no core schema (pair it with a version check first, e.g. via peek_schema_version()). This is not "no schema write at all": the manager still configures the configured vector backend after construction regardless of migrate, and with vector_backend="sqlite-vec" that creates the embedding_vec virtual table and its supporting shadow tables on first use if they are not already present — independent of ensure_schema() and of migrate. |
await peek_schema_version(service_name) | Read a service database's stamped user_version without opening it through ensure_schema() — a plain PRAGMA on a throwaway connection, so this call itself can never migrate or refuse anything. Returns None when the service has no database file yet. Raises ConfigError for an invalid name. Meant to be checked before get_store(..., migrate=False), so a caller can warn, refuse, or proceed on its own terms first. |
service_exists(service_name) | Validate the service name, then check whether its .db file exists without opening or creating it. Raises ConfigError for an invalid name. |
await list_services() | Scan data_dir for .db files and return their stems in sorted order; returns [] when the directory does not exist. |
await delete_service(service_name) | Close and evict a cached store, then delete its .db, .db-wal, and .db-shm files. Raises FileNotFoundError when the main database file does not exist. This permanently deletes that service's data. |
await close_all() | Close all cached connections and clear the cache; idempotent. Database files remain on disk, and a later get_store() opens a fresh store instance. Async context-manager exit calls this method. |
from pathlib import Path
from engrava import EngravaManager
async with EngravaManager(data_dir=Path("./data")) as mgr:
store = await mgr.get_store("my-service")
assert mgr.service_exists("my-service")
print(await mgr.list_services()) # ["my-service"]
# The context manager has closed its cached connections, but the database
# remains. Delete is a separate, destructive operation:
async with EngravaManager(data_dir=Path("./data")) as mgr:
await mgr.delete_service("my-service")Domain Models
All models are frozen Pydantic objects. For ThoughtRecord, use
thought.evolve(...) when changing lifecycle or temporal fields: it validates
lifecycle transitions, preserves created_at immutability, refreshes
updated_at unless supplied explicitly, and revalidates the complete record.
Likewise, ActionRecord.evolve(...) validates action-status transitions.
Pydantic's model_copy(update={...}) does not validate its update payload.
Reserve it for trusted, already-validated changes to fields on which no domain
invariant depends (or use model_copy() without update for an unchanged
copy). Do not use it for ThoughtRecord lifecycle or timestamp changes.
ThoughtRecord
| Field | Type | Description |
|---|---|---|
thought_id | str | UUID primary key |
thought_type | ThoughtType | Classification |
essence | str | Short summary (1-200 chars) |
content | str | Full text |
priority | Priority | P1-P4 |
lifecycle_status | LifecycleStatus | State machine status |
created_cycle | int | Creation cycle number |
updated_cycle | int | Last update cycle |
source | str | Origin identifier |
confidence | float | None | Confidence score |
embedding_ref | str | None | Embedding ID reference |
source_type | KnowledgeSource | Knowledge provenance |
confirmation_count | int | Experience confirmations |
consolidated_from | list[str] | None | Source thought IDs if consolidated |
visibility | ThoughtVisibility | Access scope |
access_count | int | Times explicitly accessed |
action_outcome_score | float | None | Mean outcome value ([0.0, 1.0]) over the thought's terminal linked actions; None when it has none. Maintained by the store when a linked action reaches a terminal state |
last_accessed_at | str | None | ISO-8601 datetime of last access |
created_at | str | None | ISO-8601 datetime when persisted |
updated_at | str | None | ISO-8601 datetime of last mutation |
expires_at | str | None | ISO-8601 datetime when the thought expires (TTL) |
valid_from | str | None | ISO-8601 start of the fact's real-world valid time (open lower bound when None); see Bi-temporal Model |
valid_until | str | None | ISO-8601 end of valid time, exclusive (open upper bound when None); see Bi-temporal Model |
metadata | dict[str, MetadataValue] | Caller-supplied structured attributes (default {}) |
provenance | ProvenanceContext | None | Optional bounded write-time provenance; an untrusted query hint, never identity or authorization |
pinned | bool | Hard keep-intent for Memory Hygiene; pinned thoughts are never auto-archived or auto-GC'd (default False) |
archived_at_cycle | int | None | Cognitive cycle stamped by hygiene archival and cleared by restore_thought; gates cycle-based GC eligibility. A generic update_thought(lifecycle_status=ARCHIVED) call leaves a previously-set value in place instead of refreshing or clearing it, so a non-null value is not by itself proof this archival episode was hygiene's |
archived_at | str | None | UTC ISO-8601 instant stamped by hygiene archival and cleared by restore_thought; gates wall-clock GC eligibility, with the same caveat as archived_at_cycle above for a generic lifecycle write |
metadata field
MetadataValue = str | int | float | bool | None | dict[str, MetadataValue]
— leaf values must be JSON-serializable scalars; nested dict values are
accepted for structured namespaces (e.g.
metadata["source"] = {"is_self": True, "confidence": "high"}). Lists,
tuples, and other rich containers are rejected at the create_thought /
update_thought boundaries with a ValueError.
Conventional keys (recommended, not enforced — callers decide what to populate):
| Key | Type | Purpose | Example |
|---|---|---|---|
role | str | Conversation role | "user", "assistant", "system" |
lang | str | Content language (ISO 639-1) | "en", "pl", "ja" |
content_type | str | Content category | "natural_language", "code", "speech" |
session_id | str | External session reference | UUID, business ID |
turn_index | int | Turn position within a session | 0, 1, 2 |
speaker | str | Named speaker (multi-party) | "Alice", "Customer" |
await store.create_thought(
ThoughtRecord(
thought_id="...",
thought_type=ThoughtType.OBSERVATION,
essence="...",
content="...",
priority=Priority.P3,
lifecycle_status=LifecycleStatus.CREATED,
created_cycle=0,
updated_cycle=0,
source="ingest",
metadata={"role": "user", "lang": "en", "turn_index": 5},
),
)Size limits. Serialized payloads above ~4 KiB emit a WARNING log;
payloads above ~64 KiB are rejected with ValueError. If the data is
genuinely large (e.g. transcripts, structured documents), store it in
content or via an external reference rather than in metadata.
Persistence. Stored as a metadata_json TEXT NOT NULL DEFAULT '{}'
column on the thought table since user_version = 11. JSON1
extension is recommended for filtering queries (json_extract(metadata_json, '$.role')).
EdgeRecord
| Field | Type | Description |
|---|---|---|
edge_id | str | UUID primary key |
from_thought_id | str | Source thought |
to_thought_id | str | Target thought |
edge_type | EdgeType | Relationship type |
weight | float | Strength (0.0-1.0) |
created_cycle | int | Creation cycle |
source | KnowledgeSource | Provenance (default EXPERIENCE) |
decay_multiplier | float | Decay rate multiplier (default 1.0) |
valid_from | str | None | ISO-8601 start of the edge's real-world valid time (open lower bound when None); see Bi-temporal Model |
valid_until | str | None | ISO-8601 end of valid time, exclusive (open upper bound when None); see Bi-temporal Model |
metadata | dict[str, MetadataValue] | Caller-supplied structured attributes (default {}); see the metadata field note below |
metadata field (edges)
Edges carry the same generic metadata field as thoughts —
dict[str, MetadataValue], default {} — with the identical value domain
(str | int | float | bool | None | dict[str, MetadataValue]; lists and
non-finite floats such as NaN / Infinity are rejected) and the same
~4 KiB warn / ~64 KiB hard-reject size limits. It is validated at the
create_edge / update_edge boundaries with a ValueError.
Unlike thought metadata, edge metadata keys carry no reserved meaning and
no conventional-key table — the field is a purely generic carrier. Engrava
assigns no domain semantics to any key, applies no metadata-driven ranking,
enforces no vocabulary, and makes no compatibility guarantee about key names,
so do not treat any key as "well-known". Engrava's own edge creators
(dreaming, derived records) write an empty {}.
Filtering. list_edges(filters=...) accepts a typed MetadataFilter — an
AND of FieldPredicates over the edge metadata — reusing the exact
machinery that backs thought-metadata filtering: operators EQ and IN only,
the restricted $ / $.key / $[0] JSONPath grammar, and a 250-predicate
cap. An edge whose stored metadata is malformed JSON never matches a non-empty
filter. This is a query refinement, not a security boundary.
from engrava import EdgeType
from engrava.domain.models.filters import FieldOp, FieldPredicate, MetadataFilter
supports = await store.list_edges(
edge_type=EdgeType.ASSOCIATED,
filters=MetadataFilter([FieldPredicate("$.subtype", FieldOp.EQ, "supports")]),
)Persistence. Stored as a metadata_json TEXT NOT NULL DEFAULT '{}' column
on the edge table since user_version = 19.
EmbeddingRecord
| Field | Type | Description |
|---|---|---|
embedding_id | str | UUID primary key |
owner_type | str | Owning entity type (currently "THOUGHT") |
owner_id | str | Owning record ID (the thought ID) |
model_name | str | Embedding model identifier |
dimension | int | Vector dimensionality |
vector_blob | bytes | Serialized vector (packed floats) |
created_at | str | ISO-8601 creation timestamp |
ActionRecord
Records an action the agent took (a tool call, a message, …), linked to the thought that prompted it, with execution and verification state.
| Field | Type | Description |
|---|---|---|
action_id | str | UUID primary key |
source_thought_id | str | The thought this action originated from |
action_type | ActionType | Action classification |
intent | str | Description of intent (min length 1) |
status | ActionStatus | Current execution status |
verification_status | VerificationStatus | Verification state |
raw_metrics_json | str | None | Optional ground-truth facts for verification |
Store methods (on SqliteEngravaCore):
| Method | Returns | Description |
|---|---|---|
await create_action(action) | ActionRecord | Persist an ActionRecord |
await update_action(action_id, *, status=None, verification_status=None) | ActionRecord | Advance a stored action's status and/or verification_status. Validates the transition when status changes (illegal jump raises InvalidTransitionError); a verification-only update is allowed even on a terminal action. Raises ActionNotFoundError when the id is unknown at the initial read. A supplied value equal to the stored one is a no-op (no write, so it cannot go stale). On a real change, raises StaleDataError if the guarded write matches no row — another guarded write bumped revision in between, or deleted the row — see Concurrency |
await get_actions(thought_id) | list[ActionRecord] | Actions linked to a thought |
ActionStatus is a state machine: PLANNED → EXECUTING → CONFIRMED / FAILED,
and PLANNED → BLOCKED → PLANNED. can_transition_to(...) / evolve(...)
enforce valid transitions on the in-memory record, and update_action(...)
enforces the same transitions when advancing the stored action (an illegal
change raises InvalidTransitionError).
When a linked action reaches a terminal state (CONFIRMED / FAILED), the
source thought's action_outcome_score — the mean outcome value over its
terminal actions, None when it has none — is recomputed. That score is also
read by the optional action_outcome dreaming signal and by Memory Hygiene's
usage-history gate and scoring path.
The per-action value is exact and depends on both terminal execution status and verification:
| Action status | Verification status | Outcome value |
|---|---|---|
FAILED | Any | 0.0 |
CONFIRMED | CONFIRMED | 1.0 |
CONFIRMED | FAILED | 0.0 |
CONFIRMED | PENDING, PARTIAL, or UNVERIFIABLE | 0.5 |
PLANNED, EXECUTING, or BLOCKED | Any | Excluded (outcome undecided) |
action_outcome_score is the arithmetic mean of those values across terminal
actions only. It remains None when a thought has no terminal action.
import uuid
from engrava import ActionRecord, ActionType, ActionStatus, VerificationStatus
action = ActionRecord(
action_id=str(uuid.uuid4()),
source_thought_id=prompting_thought_id,
action_type=ActionType.TOOL_CALL,
intent="search the web for flight prices",
status=ActionStatus.PLANNED,
verification_status=VerificationStatus.PENDING,
)
await store.create_action(action)
# advance the STORED action through its lifecycle (journaled; validates each
# transition). A terminal status recomputes the source thought's outcome score:
await store.update_action(action.action_id, status=ActionStatus.EXECUTING)
await store.update_action(action.action_id, status=ActionStatus.CONFIRMED)
# verification can still advance while the status stays terminal:
await store.update_action(
action.action_id, verification_status=VerificationStatus.CONFIRMED
)
actions = await store.get_actions(prompting_thought_id)HybridSearchResult
A frozen container of ranked results plus backend diagnostics. It is not a per-result object with score breakdowns.
| Field | Type | Description |
|---|---|---|
results | list[tuple[str, float]] | Ranked (thought_id, combined_score), highest first |
backends_used | frozenset[str] | Backends/signals available or applicable to the query (e.g. "fts5", "vector", "graph_expansion"); a backend may appear even when it returned no rows |
reflections_evicted | int | Number of REFLECTION rows removed from the final top-K window by reflection_topk_cap and backfilled with non-reflections (default 0) |
result = await store.search_hybrid("query text", top_k=5)
for thought_id, score in result.results:
record = await store.get_thought(thought_id)
...Forgetting (Memory Hygiene) types
Configured under hygiene_policy and returned by
run_hygiene — see Memory Hygiene
and Configuration → hygiene_policy.
| Type | Kind | Key fields |
|---|---|---|
HygienePolicyConfig | config | enabled, eviction_threshold, protected_priorities, signal_weights, auto_gc_enabled, gc_min_archive_age_cycles, gc_restore_window_seconds, min_inactivity_age_seconds, max_evictions_per_run, dry_run |
HygieneResult | result | archived_count, gc_count, candidates_evaluated, dry_run, would_evict, flat_signals |
EvictionReason | audit record | thought_id, keep_score, eviction_score, decay_multiplier, threshold, signals, mechanism (always "hygiene") |
Metadata Helpers
Three exported helpers build the structured metadata dict that pins a
thought's origin (self vs external, source id, language). They are pure
functions — same arguments always return an equal dict — and you are free to
pass a literal dict instead; the helpers exist to remove typo-driven shape
mismatches at the call site.
| Helper | Signature | Use for |
|---|---|---|
percept | percept(*, is_self=False, source_id=None, label=None, confidence="high", lang="en") | Input arriving from outside (user message, document) |
utterance | utterance(*, lang="en") | The agent's own output sent to the world |
thought | thought(*, lang="en") | The agent's internal cognition (reflection, plan) |
from engrava import percept
metadata = percept(source_id="user-1", label="user")
# -> {'perspective': 'percept',
# 'source': {'is_self': False, 'confidence': 'high', 'id': 'user-1', 'label': 'user'},
# 'lang': 'en', 'content_type': 'natural_language'}Pass the returned dict as ThoughtRecord(..., metadata=...).
Enums
All enums are StrEnum — JSON-serializable and stored as strings.
| Enum | Values |
|---|---|
ThoughtType | TASK, OBSERVATION, BELIEF, REFLECTION, OUTPUT_DRAFT, NOTE |
Priority | P1, P2, P3, P4 (P1 highest) |
LifecycleStatus | CREATED, ACTIVE, DONE, ARCHIVED — allowed transitions are CREATED → ACTIVE, ACTIVE → DONE, ACTIVE → ARCHIVED (direct, without passing through DONE), DONE → ARCHIVED, and the reverse ARCHIVED → ACTIVE |
EdgeType | ASSOCIATED, DEPENDS_ON, DERIVED_FROM, MESSAGE_OF, BRIDGE, CONSOLIDATED_FROM, CONTESTED_BY |
ActionType | CLI_OUTPUT, TOOL_CALL, MESSAGE, STATE_UPDATE |
ActionStatus | PLANNED, EXECUTING, CONFIRMED, FAILED, BLOCKED |
ThoughtVisibility | member names PRIVATE, SELECTIVE, PUBLIC — stored values are lowercase ("private", "selective", "public") |
KnowledgeSource | EXPERIENCE, SEEDED_LLM, DISTILLED_LLM, DREAMING |
VerificationStatus | PENDING, CONFIRMED, PARTIAL, FAILED, UNVERIFIABLE |
SplitMode | PARAGRAPH, FIXED_WINDOW — the StructuralSplitProducer split strategy (see Structural split modes) |
Exceptions
The table below defines the public types. For operational retry, repair, and store-replacement guidance, see Error handling and recovery.
| Exception | Base | Description |
|---|---|---|
EngravaError | Exception | Base for all engrava errors |
ThoughtNotFoundError | EngravaError | Thought ID not found |
StaleDataError | EngravaError | The guarded update matched no row: another guarded write bumped the row's revision, or deleted the row, before this write. Raised by update_thought / restore_thought / update_edge / update_action (on a real change) / upsert_by_hash. Nothing of the update was applied. Not a general staleness check — see Concurrency |
InvalidTransitionError | EngravaError | Invalid lifecycle state transition |
ReferentialIntegrityError | EngravaError | create_edge named an endpoint (from_thought_id or to_thought_id) that is not an existing thought |
DuplicateEdgeError | EngravaError | The directed (from_thought_id, to_thought_id, edge_type) relationship already exists |
ReadOnlyViolationError | EngravaError | Write attempt on read-only store |
EmbeddingModelMismatchError | EngravaError | The embedding identity (model name, dimension, document-prefix fingerprint) supplied to a write or to verify_embedding_model() disagrees with the identity already stored for the corpus. The identity comes from the configured provider; for store_embedding() the model name and dimension come from the model_name and vector passed in, while the document-prefix fingerprint still comes from the configured provider, so with a prefixed provider the identity is assembled from both, and the error can fire with no provider configured. Raised when an embedding is stored (store_embedding, including the auto-embed path) and by verify_embedding_model(). The identity is compared on every embedding write and on every verify_embedding_model() call, not only the first — a later write on the same store instance with a different model name, dimension, or document-prefix raises EmbeddingModelMismatchError instead of being silently accepted (see Known Limitations), except a write tagged with the internal dreaming-centroid sentinel model name (a REFLECTION centroid, not a configured provider's output), which skips the comparison entirely. A first store_embedding() call whose own write then fails locks nothing: the identity commits only together with that write actually landing, so a corrected retry is never refused by an identity a failed first call left behind. The engrava restore CLI command does not raise this class: it reports a snapshot/target model mismatch as a CLI error (exit 1) that suggests --re-embed or --skip-embeddings |
EmbeddingProviderContractError | EngravaError | A configured embedding provider does not expose dimension, a required EmbeddingProviderProtocol member. Carries provider_class / member. Raised where the core reads the member (vector search, verify_embedding_model), never at construction. A provider that has the property and fails inside it raises its own exception instead |
EmbeddingGenerationError | EngravaError | Auto-embed failed under require_embedding=True; carries the failing thought_id. If the call does not own the 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, so a caller that catches this and exits that window cleanly commits everything inserted so far, single item or the whole batch alike (not a prefix, since this error only fires after the whole batch is already inserted, see bulk_store above), while an uncaught failure rolls it back. If the call does own the outermost transaction, the path decides: create_thought/update_thought have already committed by the time this fires, so the failure cannot undo them; a standalone bulk_store instead wraps the whole batch's inserts and trailing embed call in one transaction, so this failure rolls the whole batch back regardless — including this thought — so nothing is persisted. What is left behind is path-specific and only meaningful for whatever actually committed: create_thought leaves no embedding row at all; update_thought leaves any embedding the row already had in place — if the update committed, that embedding is now stale against the new content, and the row is still findable by vector search against that outdated vector; if the row had no embedding before, it still has none, and remains unfindable by vector search; if the update instead rolled back — only possible when this call is nested inside a caller's own window and the caller lets the failure escape it — the durable state reverts to whatever existed when that outermost window opened, not merely to what this call itself started from: an earlier write to the same thought inside the same window is undone right along with it; if the thought was created inside that same window, it no longer exists at all afterward; if it already existed before the window opened, it reverts to that pre-window state, and the retained embedding matches it only if it already did — an earlier update on the same thought, before this window ever opened, whose own re-embed failed can already have left that pre-window state stale, and this rollback neither detects nor repairs that |
ExtensionMigrationError | EngravaError | Extension schema migration failed (e.g. attempted downgrade) |
ActionNotFoundError | EngravaError | Action record ID not found; carries the failing action_id |
InvalidFilterError | EngravaError | Metadata/visibility filter invalid at construction (bad value, empty filter, unsupported operator, or too many predicates) |
InvalidFilterPathError | EngravaError | Filter path does not match the allowed JSONPath grammar ($, $.key, $[0] only) |
EmbeddingQueryPrefixMismatchError | EngravaError | Active query prefix diverges from the corpus pairing, silently degrading ranking |
JournalIntegrityError | EngravaError | On-open journal check (journal.verify_on_open) found a broken hash chain; carries first-broken diagnostics |
DerivedRecordError | EngravaError | The derived-records seam rejected a producer result (over-cap return, or a derived identity colliding with its source); carries the failing source_thought_id |
SourceThoughtNotFoundError | EngravaError | derive_existing targeted a source thought_id that does not exist; carries the failing thought_id |
VectorDimensionMismatchError | EngravaError | A vector-search query vector's length differs from the store's embedding dimension; carries expected / actual. Not a ValueError — catch this typed error (or EngravaError). |
CycleProviderError | EngravaError | A configured cycle provider returned a boolean, non-integer, or negative value |
RecencyModeConflictError | EngravaError | A query explicitly supplied both current_cycle and recency_now |
InvalidRecencyArgumentError | EngravaError | recency_now is malformed or recency_now_half_life is not positive |
ConnectionQuarantinedError | EngravaError | The connection was quarantined because a rollback or unwind this store depends on could not be proven to have completed, leaving the transaction potentially indeterminate; the store instance is terminal and must be replaced. Reachable when an outer transaction rollback fails (including recovery after a failed commit), and from independent guarded-unwind paths: the savepoint unwind of a guarded write failing (a derived child's own row or edge insert included), and any failure of delete_thought's (or the TTL delete strategy's, or hygiene GC's) child-row savepoint unwind — not only a cancellation racing the savepoint release, but also an ordinary error (e.g. a trigger veto) whose own ROLLBACK TO is then itself unable to complete, with no cancellation and no derivation involved. Reachable with the derived-records seam disabled, since neither of the delete-path triggers touches that seam. The triggering call preserves its original failure unless cleanup cancellation takes precedence; ConnectionQuarantinedError surfaces on subsequent guarded operations, including queued writers. A close timeout can itself raise it |
WriteContentionError | EngravaError | Two distinct raise sites. (1) The dedup probe-and-insert window (create_thought(deduplicate=True), get_or_create, upsert_by_hash, and bulk_store(deduplicate=True) per row — sharing the same internal dedup-insert path create_thought(deduplicate=True) uses, not a call to the public create_thought() itself — but bulk_store's whole batch runs inside one transaction, so only its first row opens this window; later rows in the same batch share the window the first row already holds) could not get the cross-connection write lock: PRAGMA busy_timeout already waits out normal contention on each BEGIN IMMEDIATE attempt, and the store's own bounded retries with backoff are exhausted on top of that (attempts counts these). (2) A revision-guarded write (update_thought, restore_thought, update_edge, update_action) reports lock contention on its single UPDATE; there is no application-level retry here, so attempts is always 1. Carries operation and attempts; from bulk_store, operation reads "create_thought" (that shared path's fixed label, not evidence the public method ran), not "bulk_store" — a caller branching on operation should account for that. Retrying the call outright is safe in both cases — nothing was read or written under the failed attempt |
WriteLockTimeoutError | EngravaError | A task could not acquire the store's in-process write lock within write_lock_acquire_timeout_seconds. Usually a task spawned and awaited from inside another task's own suspend_auto_commit() window (an out-of-contract deadlock this store ends by raising, rather than hanging); can also mean that bound is configured too small for a legitimately slow embedding provider — see Concurrency |
DedupLockReentryError | EngravaError | The current task already holds the store's dedup lock and tried to acquire it again — same-task reentry into create_thought(deduplicate=True), get_or_create, upsert_by_hash, or bulk_store(deduplicate=True) while one of those is still mid-window on this task, most concretely an update_thought override (reached from upsert_by_hash's hit branch, which calls it when a mutable field differs, while _write_lock and _dedup_lock are held) that calls back into one of those same four entry points. A plain, non-reentrant lock would block that second acquisition on itself forever; this is raised instead, the same "raise, don't hang" policy WriteLockTimeoutError applies to a different task's wait — see Extension hooks §1B.3 |
SchemaVersionError | EngravaError | ensure_schema() refused the database: a populated sub-floor schema (below the bootstrap floor, but already carrying a row — not safe to silently stamp current), a sub-floor schema whose core tables hold zero rows but already exist under an older shape the bootstrap script's CREATE ... IF NOT EXISTS statements would leave untouched, or one stamped above this build's head version (nothing to migrate; opening it would understand less than it claims to). Carries current_version and reason ("populated_sub_floor", "stale_shape_sub_floor", or "newer_than_head"). Reachable anywhere ensure_schema() runs, which includes from_config(), EngravaManager.get_store(), and the CLI's migrate command. See Deletion on a database that has not been migrated |
CoreMigrationError | EngravaError | ensure_schema() upgraded the on-disk schema but a migration step's own postcondition (a required column, table, index, foreign key, or trigger) did not hold afterward. Carries the target schema version and what was missing. The version is not stamped past the last fully-applied step, so the next call to ensure_schema() retries the remaining steps rather than silently treating a half-migrated database as current. In practice this means the on-disk file or the running build is not one this version of engrava can safely bring current — replace the database from a backup, or use a build that matches its schema history. Reachable anywhere ensure_schema() runs: from_config(), EngravaManager.get_store(), and the CLI's migrate command |
ConfigError | ValueError | YAML or direct config construction violates a documented configuration invariant |
MindQLParseError | Exception | parse() rejects invalid MindQL syntax — see Discovery and parsing helpers |
MindStoreError | Exception | Deprecated compatibility alias. It does not derive from EngravaError, it is EngravaError — the same class object, resolved lazily through engrava.__getattr__, which emits a DeprecationWarning on access. Its base is therefore Exception, like the class it aliases. New code should catch EngravaError directly — see Deprecated compatibility aliases |
create_edgeraisesReferentialIntegrityErrorwhen an endpoint thought does not exist andDuplicateEdgeErrorwhen the relationship already exists. All three ofReferentialIntegrityError,DuplicateEdgeError, andCoreMigrationErrorare importable directly fromengrava, the same as every other exception in the table above.
Protocols
EngravaReadProtocol
The non-mutating capability shared by the writable store and
ReadOnlyEngrava: thought/edge/action/embedding reads, FTS/vector/hybrid
retrieval, recall, metrics, and max_cycle. Read-only retrieval suppresses
the concrete SQLite store's optional access-frequency instrumentation.
ReadOnlyEngrava declares and type-checks against this read-side protocol. Its
compatibility write methods are blocked and raise ReadOnlyViolationError.
EngravaCoreProtocol
The writable abstract interface for an engrava implementation. It extends
EngravaReadProtocol, the separately exported read capability implemented by
both SqliteEngravaCore and ReadOnlyEngrava. Because runtime structural
checks inspect member names and ReadOnlyEngrava exposes rejecting write
methods, the wrapper also satisfies isinstance(ro, EngravaCoreProtocol).
That result describes structural compatibility, not permission to write; use
the narrower read protocol for capability boundaries.
CycleProvider
A runtime-checkable, synchronous pull protocol with one method:
current_cycle() -> int. It supplies a cognitive cycle only when a supported
call omits an explicit one and no explicit recency_now selects transaction-time
recency. It never advances a cycle, never stamps writes, and is never loaded from
YAML. The store validates each pulled value as a real, non-negative int.
| Implementation | Construction | Behavior |
|---|---|---|
StaticCycleProvider | StaticCycleProvider(value) | Always returns one fixed value |
CallableCycleProvider | CallableCycleProvider(fn) | Calls a zero-argument consumer function on every pull; purity is the consumer's responsibility |
MaxCycleProvider | await MaxCycleProvider.create(store) | Caches await store.max_cycle(); current_cycle() may be stale until await refresh() |
EngravaHooksProtocol
Extension hook interface — see Extensions.
DerivedRecordProducerProtocol
Optional capability: return N derived records from one stored thought. Core
persists each as an ordinary thought after the source is durable, through a
core-owned, guarded, per-child lifecycle. See
Extension hooks §1A.
class DerivedRecordProducerProtocol(Protocol):
async def derive_records(
self, thought: ThoughtRecord, ctx: DeriveContext
) -> Sequence[DerivedRecord]: ...Companion generic types (all public, X.Y.x-stable): DerivedRecord
(producer-owned non-empty content / thought_type / priority / metadata /
attach_provenance_edge; core derives the essence and owns identity),
DeriveContext (source id, content hash, cycle, informational origin; no
store handle), DeriveGates
(enabled, on_error, max_derived_per_source), and DeriveResult (the
created / reused / skipped tally returned by
derive_existing). Configure via the derive:
YAML section or the derive_gates= constructor argument. The shipped reference
producer is StructuralSplitProducer (see
Structural split modes).
EmbeddingProviderProtocol
class EmbeddingProviderProtocol(Protocol):
@property
def dimension(self) -> int: ...
@property
def model_name(self) -> str: ...
async def embed(self, text: str) -> list[float]: ...MindQL
See MindQL for the query language reference.
execute_mindql (on the store)
SqliteEngravaCore exposes a convenience that runs a MindQL query directly
against the store's own connection, so you don't have to construct a
MindQLExecutor yourself. Like the executor, it takes a parsed
MindQLQuery — parse() the string first.
| Method | Returns | Description |
|---|---|---|
await execute_mindql(query, *, extensions=None) | MindQLResult | Run a parsed MindQLQuery on the store's connection. extensions is an optional dict[str, MindQLExtension] registering extension commands. |
from engrava import parse
result = await store.execute_mindql(
parse("FIND thoughts WHERE lifecycle_status = 'ACTIVE' LIMIT 10")
)
print(result.rows)MindQLExtension, ReadOnlyAccessor, and ReadOnlyCursor
MindQLExtension registers a custom MindQL command: command_name,
handler, description, and category (defaults to "extension"). The
executor invokes handler with two positional arguments — a
ReadOnlyAccessor and the parsed argument list — and awaits a
list[dict[str, object]] back. See
Custom MindQL Commands for a worked
example.
A handler receives a ReadOnlyAccessor, never the store's live connection —
its only capability is execute(), restricted to a single SELECT:
| Type | Member | Description |
|---|---|---|
ReadOnlyAccessor | await execute(sql, parameters=()) -> ReadOnlyCursor | Runs one SELECT statement through the same guard as the SELECT passthrough; raises MindQLParseError for anything else. |
ReadOnlyCursor | fetchone(), fetchall(), fetchmany(size=None), async for row in cursor, .description | The read surface of an aiosqlite.Cursor and nothing else — no execute()-family method, no connection. |
See Extension Commands for the read-only guarantee this closes off.
MindQLExecutor
MindQLExecutor runs against an open aiosqlite.Connection (the same
connection the store wraps), and execute() takes a parsed MindQLQuery
— parse the string first.
from engrava import MindQLExecutor, MindQLResult, parse
executor = MindQLExecutor(conn) # an aiosqlite.Connection, not a store
result: MindQLResult = await executor.execute(
parse("FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 10")
)
print(result.rows) # list[dict]
print(result.count) # int | None (set for COUNT queries)MindQLResult fields: columns: list[str], rows: list[dict],
count: int | None, command: str.
parse
parse() returns a MindQLQuery plan (not a MindQLCommand). The grammar
requires a table name and an optional WHERE clause:
FIND <table> [WHERE <field> <op> '<value>' [AND ...]] [LIMIT n].
from engrava import parse, MindQLQuery
query: MindQLQuery = parse("FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 5")
print(query.command) # MindQLCommand.FIND
print(query.table) # "thought"
print(query.conditions) # list of parsed conditions (field / operator / value)
print(query.limit) # 5See MindQL for the full grammar, operators, and per-table filterable columns.
Additional top-level exports
The sections above cover the primary store, records, protocols, and query
surface in depth. The remaining names exported directly from engrava are
listed here so the top-level package has a complete index; follow the linked
specialist guide for operational detail.
Configuration objects and resolvers
| Export | Purpose |
|---|---|
EngravaConfig | Root typed configuration returned by load_config |
SearchConfig | Hybrid-search weights, half-lives, graph/reflection controls, and candidate-pool bounds |
DreamingConfig, DreamingGates | Consolidation settings and eligibility gates |
EmbeddingConfig | Embedding provider and model settings |
HygienePolicyConfig | Forgetting / Memory Hygiene policy |
JournalConfig | Journal enablement and verify-on-open settings |
MetricsConfig | Metrics enablement and rolling latency-window size |
TTLConfig | Expiry strategy, cadence, and default TTL |
ServiceConfig, ServicesConfig | Per-service and multi-service manager configuration |
load_config(path) | Parse and validate YAML into EngravaConfig |
resolve_embedding_provider(config) | Construct the provider selected by an EmbeddingConfig |
resolve_hooks(hooks_class) | Import and construct a dotted-path hooks class, or return default hooks for None |
resolve_manifests(manifest_paths, *, discover=False) | Load explicit manifest paths and optionally append entry-point discovery results |
See Configuration for the fields, defaults, validation, and YAML examples.
Embedding and vector implementations
| Export | Purpose |
|---|---|
CallbackProvider | Adapt caller-supplied embedding callbacks to EmbeddingProviderProtocol |
SentenceTransformerProvider | Local sentence-transformers provider |
HuggingFaceProvider | Hugging Face inference/provider adapter |
OllamaProvider | Ollama embedding provider |
OpenAICompatibleProvider | Provider for OpenAI-compatible embedding endpoints |
RoleAwareEmbeddingProvider | Optional protocol for distinct query/document embedding paths |
SqliteVecSearchBackend | sqlite-vec vec0 backend used by configured stores |
See Embeddings and Performance before selecting a provider or vector backend.
Dreaming and scoring types
| Export | Purpose |
|---|---|
DreamingExtension | Consolidation implementation. run_consolidation() runs immediately; is_due() / run_if_due() apply the configured cycle cadence without starting a background scheduler. |
DreamingContext | Per-run context supplied to dreaming signals |
DreamingSignalProtocol | Contract implemented by promotion-scoring signals |
ActionOutcomeSignal, ConfidenceSignal, ConfirmationSignal | Outcome, confidence, and confirmation scoring signals |
FrequencySignal, RecencySignal, StalenessSignal | Access-frequency, cycle-recency, and staleness scoring signals |
ScoringContext | Context type accepted by the reserved hook-level score_function |
See Dreaming for activation, gates, signal semantics, and the no-retrieval-accuracy-claim boundary.
Journal, cleanup, and metrics value objects
| Export | Purpose |
|---|---|
JournalEntry | One persisted hash-chain journal entry |
JournalWriter | Low-level journal writer bound to an open connection; ordinary callers use store.journal / verify_journal() |
MutationType | Journal mutation-kind enum |
CleanupStrategy | TTL cleanup enum: ARCHIVE or DELETE |
ThoughtCounts, EdgeCounts | Count components nested in EngravaMetrics |
StorageFootprint | Database, WAL, vector-index, and total byte counts |
LatencyHistogram | Rolling search-latency count and percentile summary |
Discovery and parsing helpers
| Export | Purpose |
|---|---|
discover_manifests() | Discover opt-in extension manifests from package entry points |
MindQLParseError | Raised when parse() rejects invalid MindQL syntax |
Deprecated compatibility aliases
CoreThoughtRecord is a compatibility alias of ThoughtRecord. The legacy
MindStore-era names below resolve lazily to their Engrava equivalents and emit a
DeprecationWarning; new code should not use them.
| Deprecated export | Use instead |
|---|---|
SqliteMindStoreCore | SqliteEngravaCore |
MindStoreManager | EngravaManager |
MindStoreConfig | EngravaConfig |
MindStoreError | EngravaError |
MindStoreCoreProtocol | EngravaCoreProtocol |
MindStoreHooksProtocol | EngravaHooksProtocol |
DefaultMindStoreHooks | DefaultEngravaHooks |
ReadOnlyMindStore | ReadOnlyEngrava |
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