Architecture¶
grag is a local graph memory layer for an agent harness. It stores code structure, documents and authored knowledge, then returns relevant records and connections with citations. The harness decides what to ask, what to remember and how to answer the user. grag does not run an LLM or autonomously create a project ontology.
This describes grag 0.11.0, including the memory UI and updated agent guidance. The usual setup is one database per checkout, one owning process, and any number of clients using that process. BM25 works without an embedding model.
Product boundary¶
The core workflow is ingest, retrieve and remember:
- Ingest selected source files into cited, refreshable records and relationships.
- Retrieve useful evidence within a stated budget, with explicit coverage and completeness limits.
- Remember sourced facts, decisions and handoffs, then recall or correct them across sessions.
The agent harness owns planning, model execution and tool selection. Authored memory uses the project's vocabulary; grag does not require a task engine, ontology or schema preset. Embeddings and the browser views are optional ways to use the same local graph.
Single-owner access, atomic writes, edit guards, backup and recovery support the core's reliability. Their correctness is required whenever those paths are used. Additional administrative workflows can wait for demonstrated user needs.
Evaluate proposed features against a concrete failure or repeated user friction. Prefer improving existing tools and defaults. New setup steps, services, models, tools or mandatory concepts need evidence that their benefit outweighs their operational and context costs. Measure agent task correctness alongside tokens, latency and setup effort; smaller tool replies alone do not establish session savings.
The usual local setup¶
flowchart TB
Agents["Claude Code, Cursor and other MCP clients"]
Proxies["grag stdio proxies · one per client"]
Clients["Browser UI and CLI graph commands"]
Agents -->|stdio| Proxies
subgraph Owner["One owning grag server process"]
MCP["HTTP MCP tools"]
API["REST API and web UI"]
Registry["ServiceRegistry"]
Service["GragService · one per database"]
Work["Ingestion, retrieval and background work"]
Engine["Engine · serialized writer and pooled readers"]
MCP --> Registry
API --> Registry
Registry --> Service
Service --> Work
Service --> Engine
Work --> Engine
end
Proxies -->|HTTP on loopback| MCP
Clients -->|HTTP on loopback| API
Engine --> DB[("LadybugDB · .lbdb and recovery sidecars")]
grag init saves the checkout's database and port in .grag/project.json and
configures the harness. Its default stdio proxy starts or connects to a shared
serve --with-mcp process. Proxies never open the database. Disconnecting a client
leaves the owner available to the other clients. On Windows, a harness that denies
independent daemon creation requires the printed separate-terminal server command.
The first-use path in 0.10.0 adds a portable user-level skill and
init --ingest-if-empty. The CLI checks full graph counts and the init marker,
then uses the same GraphClient ingestion route only for an empty/setup-only graph.
It adds no daemon or MCP tool; first-use setup
describes the eligibility check and harness discovery limits.
How clients reach the owner¶
| Access mode | Database ownership |
|---|---|
| Init-generated MCP configuration | Each client has a stdio proxy; one shared server owns the file. |
| Browser UI or direct HTTP MCP | Connects to the owning server. |
CLI remember, inspect, retire, search, context, ingest, ingest-code |
Uses a registered owner's REST API, or a configured remote server. Opens a local service only when no owner is selected. A failed server request never falls back to another writer. |
Direct grag --db file.lbdb mcp |
The stdio process owns the file; suitable for one client. |
Python GragService(config) |
The Python host owns the file and must close the service it creates. It does not automatically attach to another owner. |
--db-dir server |
One registry manages separate services/files. Database selectors route requests; there are no cross-database queries. |
Separate checkouts/worktrees get separate identities by default. Sharing a file is explicit and still requires a single owner. An optional HTTPS deployment uses the same server and proxy arrangement on another host; there is no separate distributed database layer. See projects and servers and multiple clients.
Browser memory workflow¶
The browser's Memories view lists stored records through a bounded, schema-aware
read in memory_view.py. It does not create concepts, decisions or a required
memory schema. History inspection uses the existing context API; corrections and
reviews use the existing node upsert with revision guards and retry receipts.
The Graph view retains Cypher and relationship exploration, while Health observes
the selected database's registered code roots, jobs and optional embedding worker.
See review memory in the UI.
Full SVG export captures only node keys/labels and relationship endpoints to a temporary file under the serialized writer lock. The file is delivered after releasing native readers and the lock, so a slow download does not hold up writes. Ordinary query reply limits remain unchanged; this drawing is not a data backup.
Responsibilities in the code¶
| Component | Responsibility and source |
|---|---|
| Setup and routing | project.py resolves the checkout; admin.py manages registered owners; client.py routes CLI graph operations. |
| Client transports | proxy.py bridges stdio and HTTP; mcp_server/server.py exposes tools; api/main.py exposes REST and the UI. |
| Shared service | registry.py reuses one service per resolved database path; service.py handles admission, freshness, bounded operations and shutdown. |
| Storage and mutations | core/engine.py owns connections and transactions; core/mutations.py, core/evidence.py and core/revisions.py implement retry receipts, history and edit guards. |
| Ingestion | ingest/ selects files, parses supported structures and reconciles generated records while preserving authored evidence. |
| Retrieval | retrieval/ builds lexical/vector candidates, ranks seeds, expands the graph and packs bounded context. |
| Background work | jobs.py serializes queued jobs per database; refresh.py coordinates source verification; embedworker.py runs optional embedding work. |
| Portability and recovery | transfer.py captures/restores logical snapshots; recovery.py preserves damaged files and attempts replay on a separate copy. |
What persists¶
LadybugDB stores typed node and directed relationship tables. Agents can define
domain-specific labels and keys; there is no mandatory memory vocabulary.
describe_schema reports the current schema. Canonical node IDs use Label:key,
not native row IDs.
In grag 0.10.0, define_schema validates unquoted identifiers against
the pinned native grammar and publishes DDL plus its registry in one writer
transaction. Failed batches roll back their earlier tables and metadata; an
enclosing transaction is joined without savepoints. Schema readers share the
writer lock and do not observe half a batch. See schema naming.
| Data | Lifetime and meaning |
|---|---|
| Authored memories | Facts, decisions, tasks and relationships persist in the selected database. Sources and optional correction history explain where a claim came from. |
| Code structure | Repo, Module, Class, Function and language-specific nodes such as Go Constant and TerraformModuleCall, with supported structural relationships. Stored signatures, docstrings, expressions and citations complement source files; function bodies are not copied into the graph. |
| Documents | Document/Section/Chunk graphs and code mentions, or flat document records. Re-ingestion reconciles loader-owned data; authored relationships remain protected. |
| Edit history and retry receipts | Durable database records. Format-2 logical snapshots preserve them, including receipt replay and portable relationship revisions. |
| Search indexes and vectors | Derived retrieval data. Rebuildable from the stored text and configured embedding policy; logical restore rebuilds these rather than treating them as authored evidence. |
| Jobs and worker counters | Process-local state. A restart loses job records; accepted committed graph changes remain durable. |
The checkout mapping and daemon registrations/logs are filesystem metadata outside
the graph. A .lbdb may have WAL and shadow/checkpoint sidecars needed for recovery;
copying only the main file during live writes is not a backup.
How changes become durable¶
Schema definition is separate from an authored mutation. An upsert validates its
batch and optional revision guards, then commits nodes, edges, history and the
optional operation receipt together on the serialized writer. Ordinary statement
failure rolls back the batch. An exact retry with the same operation_id replays
the original result; it does not undo later edits. Competing revisions require a
reread and reconciliation. See memory writes.
Each engine has one write connection and a reader pool (default maximum: four). Other threads read committed state while a transaction's own reads use its writer. Limits bound admitted operations and decoded query work; they are not a hard process-memory ceiling. grag 0.10.0 pins LadybugDB 0.20.3, with grag's prepared-plan and native timeout safeguards retained. Ordinary statements default to a cooperative 30-second native limit; commit, rollback and checkpoint finish without that limit. An uncertain transaction outcome blocks further writes until reopen.
Ingestion and source freshness¶
Code ingestion selects the requested roots using .gitignore, .gragignore,
nested repository/worktree boundaries and size limits. Python uses stdlib ast;
the optional code extra supplies tree-sitter parsers. Resolution is conservative
static analysis. Coverage diagnostics distinguish supported, unresolved and
omitted constructs; missing edges are not proof of absence.
Parsing happens before the graph transaction. grag 0.10.0 adds a bounded owner-local cache of parse summaries, keyed by source bytes, parser/version/options, repository identity and path. Copies keep resolver mutations out of cached summaries. The cache retains no source bodies or native syntax trees and clears on owner shutdown. Every scan still applies selection rules, reads source bytes and resolves current dependencies. Separate components in the existing stored fingerprint distinguish source changes from dependency-only module/edge updates. Legacy fingerprints cause one conservative rewrite. Publication, pruning and successful scope metadata commit together; cached parsing never certifies either publication or freshness. Unreadable or partial scans do not certify a complete index. See the code guide for cache limits and response counters.
Serving reads trigger coalesced verification jobs for registered code scopes.
allow_stale returns existing evidence while a due check runs; wait waits to a
deadline; require rejects the read if verification cannot finish. An idle server
does not poll continuously. Freshness certifies the code scope at checked_at,
not the truth of memories or completion of embeddings. Document synchronization
is explicit: rerun document ingestion after edits.
New in 0.10.0: the file loader's explicit json_mode="document" validates
ordinary JSON and hands its literal text, file provenance and source hash to the
same document pipeline. This adds no semantic JSON graph or reference resolver;
the default JSON/JSONL document-record format is preserved.
Details: code coverage, documents and freshness.
From a question to context¶
- For exact structural questions, read-only Cypher projects the needed facts. For discovery, search builds BM25 candidates from searchable text tables.
- With an embedder configured, search also scores vectors. Default
fp32uses exact cosine; optional compressed codecs shortlist candidates and rescore them. Native HNSW acceleration remains disabled. - Search combines reciprocal ranks and applies label diversity, then expands selected seeds through graph relationships. Current-evidence filtering removes explicitly obsolete, superseded, retracted, expired and disputed records.
- Packing returns cited records, connecting edges and selected text under the requested budget, with explicit truncation, omissions and paging metadata.
get_context starts from known IDs instead of search candidates. Neither path
generates an LLM answer. An exact vector score does not make the overall answer
complete: scope, candidate selection, parser coverage and packing still limit the
evidence. Budgets use a UTF-8 byte estimate, not a model tokenizer guarantee.
See retrieval and resource limits.
Lifecycle, recovery and trust¶
The optional embedding worker computes outside the writer lock and only commits results whose source text and configuration still match. Query embedding still runs as part of each semantic search. Code verification and background ingestion share a single job queue per database. Shutdown cancels queued work and drains accepted operations before closing native connections. HTTP transport cleanup and application drain have separate bounded waits; work that outlasts the application grace period keeps the database open until it exits. A proxy reconnects its MCP session after a restart; an interrupted write still needs receipt replay or stored-state inspection, not a guessed new write.
Online export captures one committed state into a completed spool, then streams it. Restore validates and strictly reopens a new destination before publication. Recovery of an unreadable database preserves original files and works on copies; partial replay requires an explicit choice. See backup and recovery.
Storage and retrieval are local by default. First-use extensions, grammars and
optional models can need downloads; doctor --prepare prepares those assets.
Provider boundary¶
The harness may send retrieved context to its model provider, and an explicitly remote embedder sends embedding input to its configured endpoint. Shared HTTP uses a bearer token when configured and requires one beyond loopback. Database selectors and caller-supplied evidence actors are not per-user authorization. See installation and server security.