Memory Architecture¶
The tatara memory system is a persistent knowledge graph of your codebase. It replaces the cold-read pattern (agent reads files from scratch on every turn) with a queryable graph that survives session boundaries, pod restarts, and code changes.
The name alludes to the platform metaphor: the tatara forge's permanent substrate that every ephemeral agent session works against.
Stack¶
tatara-memory (REST service, Go)
|
v
LightRAG (Python, upstream library)
| |
v v
Neo4j CNPG Postgres
(authoritative (LightRAG KV + vectors:
graph store, PGKVStorage +
Neo4JStorage) PGVectorStorage;
doc + ingest status)
LightRAG is configured with LIGHTRAG_GRAPH_STORAGE=Neo4JStorage, PGKVStorage, and PGVectorStorage. Neo4j holds the entity/relationship graph; Postgres holds the key-value store, vector embeddings, and document/ingest status. tatara-memory stores no live conversation state - there is no session-resume mechanism at all (see Agent Execution). What it does store is the overflow from Task, Issue, and MergeRequest objects in etcd, once those hit their own byte budgets: see Task continuity spill below.
One stack is provisioned per Project CR by the operator. Sizes are tunable via spec.memory:
spec:
memory:
pgInstances: 3 # CNPG replicas (1=dev, 3=HA)
pgStorage: 20Gi
pgWalStorage: 10Gi # dedicated WAL volume, separate from PGDATA (default 8Gi)
neo4jStorage: 10Gi
How memory is populated¶
flowchart TD
A[Repository CR created] --> B[Operator creates ingest Job]
B --> C[tatara-memory-repo-ingester\nclones + walks repo]
C --> D{Language analyzer}
D --> E[AST entities + edges]
D --> F[Semantic text chunks]
E --> G[tatara-memory REST API]
F --> G
G --> H[LightRAG]
H --> I[(Neo4j\ngraph store)]
H --> J[(Postgres\nKV + vectors)]
K[Push webhook] -->|annotates Repository CR| L[Incremental ingest Job\n--since lastIngestedCommit]
L --> C
M[Cron reingestSchedule] --> L - Initial ingest: Repository CR created -> operator creates an ingest
Job->tatara-memory-repo-ingesterclones + bulk-posts chunks and graph entities. - Incremental ingest: push webhook annotates the Repository CR; the operator creates a new ingest Job running
--since <lastIngestedCommit>(delta only). - Scheduled re-ingest:
spec.reingestSchedule(cron expression on the Repository CR) triggers a periodic incremental catch-up (--since lastIngestedCommit), guarding against missed webhooks. It is not a full rebuild; a full ingest happens only on first ingest or after repeated incremental failures (e.g. a force-pushed branch whose since-SHA is gone). - Semantic extraction: when
spec.semanticIngest: true(default) and an OpenAI Secret is configured, each changed file is also processed by OpenAI for LLM-powered entity and relationship extraction (SEMANTIC_MODEL, defaultgpt-4o-mini; the key comes from the OpenAI Secret, keyLLM_BINDING_API_KEY). This enriches the Neo4j graph beyond what AST analysis alone produces. It is not Claude: with no OpenAI Secret provided the ingester runs AST-only and does not fail.
How agents query memory¶
Inside agent pods, tatara-cli mcp exposes the memory-facing part of the MCP tool surface. Nine of the twenty-one tools read or write memory: five memory_* tools against the LightRAG entry points, and four code_* tools against the code graph.
| MCP tool | What it does |
|---|---|
memory_query | Retrieve memory references for a query (POST /queries) |
memory_describe | Generative answer plus source paths for a query (POST /queries:describe) |
memory_write | Insert a text memory; returns its track_id |
memory_entity | Read, search, or patch one knowledge-graph entity (op=get, op=search, op=patch) |
memory_edges | List, create, or delete edges between entities (op=list, op=create, op=delete) |
code_search | Search one repository's code graph for entities (optional type filter) |
code_context | One entity's neighborhood, selected by rel=: entity, neighbors, callers, callees, dependents, dependencies, file_imports, related, cross_repo |
code_graph | Whole-graph analyses, selected by op=: path, important, stats, ambiguous, communities, hyperedges, bridges, resource_graph |
code_explain | Explain one entity: what it is, what it touches, why it matters |
The relation traversals and whole-graph analyses that were once separate code_* tools are now rel= and op= arguments; code_context and code_graph are the only entry points to them. repo is required on every code_* call.
The LightRAG query mode is an argument, not an inference. mode is required on memory_query and memory_describe, has no default, and must be one of naive, local, global, hybrid; tatara-memory rejects an unrecognized mode before it reaches LightRAG.
Not every pod sees all nine. tools/list is served per profile from TATARA_TOOL_PROFILE, so a refine pod gets no code_* tools at all and a review pod gets only memory_query and memory_describe. The per-profile grid is in MCP Tools by Agent Kind.
Task continuity spill¶
There is no session-resume mode: --resume, a stored session ID, and a stored S3 transcript object key are all gone (Task.status.sessionID and Task.status.conversationObjectKey no longer exist). Every pod's turn-0 gets an identical, freshly rendered context bundle from current CR state; the state that actually carries forward between pods for the same Task is Task.status.notes, an append-only journal (see Agent Execution). Two things overflow etcd's per-object byte budget and spill into tatara-memory instead of being dropped:
- Notes beyond the 50-item Go-side cap on
Task.status.notes. The oldest notes spill first;Task.status.stats.notesSpilledcounts them andTask.status.stats.notesSpilledRefsaccumulates onetrack_idper spill batch. They are read back via thetask_context(notes=all)MCP tool - a spilled note that could not be read back would be continuity silently lost, so the read path always includes spilled notes. - Evicted
Issue/MergeRequestcomments, once the object's marshaled size would exceed its byte budget (a pre-write guard, not a count cap: 200 comments of 40 KB each blows well past a count-only ceiling). The oldest comments are spilled first,status.commentsRetainedFromadvances past what was evicted, and the spill write happens once, outside any retry loop, before the trimmed object is ever written - so a spill failure aborts the write instead of silently losing comments.
Both spills are one-way: nothing pulls old notes or comments back out of tatara-memory into the CR. They exist so the agent-facing tools can still retrieve the full history on demand without the CR itself growing past its etcd object ceiling.
On a spec.memory.enabled: false Project there is no tatara-memory to spill into, so overflow is discarded instead of spilled (counted in operator_objbudget_evicted_dropped_total, not treated as an error) - the same "no 409/503 on cap" invariant as a memory-enabled Project, just with nothing retrievable past the cap rather than a spill ref. Genuinely unconfigured memory (enabled but not yet provisioned) still blocks and retries, since there is something to wait for there.
Durability considerations¶
| Concern | Mitigation |
|---|---|
| CNPG pod restart | CNPG manages HA; use pgInstances: 3 for production. Postgres holds LightRAG KV + vectors, so a lost Postgres loses embeddings and requires re-ingest. |
| Neo4j pod restart | Neo4j is the authoritative LightRAG graph store, not a projection. Its PVC holds durable graph data; there is no rebuild-from-Postgres path. Recovery from a lost Neo4j volume is re-ingest of every repo, not an automatic rebuild - do not delete the Neo4j PVC expecting it to repopulate. |
| CephFS write-cap leak | Known fragile under unclean probe-kill restarts (CNPG io_method=sync); consider RBD for CNPG PVCs in Ceph environments |
LightRAG duplicated response | Treated as success; re-ingesting the same chunk is idempotent |
LightRAG busy response | Treated as transient; controller retries with exponential backoff |
| Stale page cache (Neo4j EIO) | Restart the Neo4j pod; the error is poisoned page-cache from Ceph OSD crashes, not data loss |