Skip to content

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
  1. Initial ingest: Repository CR created -> operator creates an ingest Job -> tatara-memory-repo-ingester clones + bulk-posts chunks and graph entities.
  2. Incremental ingest: push webhook annotates the Repository CR; the operator creates a new ingest Job running --since <lastIngestedCommit> (delta only).
  3. 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).
  4. 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, default gpt-4o-mini; the key comes from the OpenAI Secret, key LLM_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.notesSpilled counts them and Task.status.stats.notesSpilledRefs accumulates one track_id per spill batch. They are read back via the task_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/MergeRequest comments, 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.commentsRetainedFrom advances 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