The Memory Engine is the core component of GoClaw responsible for persistent long-term knowledge, session-based context management, and semantic retrieval. It provides a hybrid search system that blends traditional keyword search with vector-based semantic retrieval.
This page covers the engine's internals. Memory scopes and who may write them, what goes into the prompt, the search settings and the memory lifecycle are described in Memory & RAG.
The memory system uses a combination of structured and unstructured storage:
Memory.md) and session archives (memory/archive/).internal/store, gc_state.sqlite, WAL mode; PostgreSQL, MySQL or ClickHouse with database.driver).memories table: All records (global, personal, agent_notes, agent_config, and the internal wiki and db_cache scopes).memory_chunks table: Indexed text fragments with vector embeddings.kg.sqlite in the state directory (or PostgreSQL with memory.graph.backend: postgres), holding graph_entities and graph_relations for graph queries.memory/archive/.The engine uses Reciprocal Rank Fusion (RRF) to combine results from two distinct search algorithms:
The final score for each chunk is calculated as:
RRF Score = 1 / (60 + rank_vector) + 1 / (60 + rank_keyword).
To ensure the most relevant and "fresh" information surfaces:
e^(-0.015 * (days - 14)), with a floor of 0.4.memory_search) get a small boost: +0.002 * access_count (capped at 10 accesses).\n\n), then single lines, and finally sentences to preserve semantic integrity.sha256(source + startLine + content), allowing the system to reuse cached embeddings when unchanged content is re-indexed.The engine automatically handles context window limitations through the session summarizer (session_summarizer.go), which runs after a turn:
memory.session_memory.max_tokens (default 40,000) or, with that set to 0, memory.session_memory.messages (default 40). While the memory flush is on, the history kept is also held to half its threshold (sessionTokenLimit): a turn starts with that history, and one that started past the threshold would be flushed at its first call and answered without any of it. The limit is computed over the whole history, so a session however far over it is back within it after one summarize.historyTrimShare), and the summary is carried into later turns as PREVIOUS CONTEXT. The turns after add to the history at its end until it passes the limit again, so the start of the conversation, from which every provider's prompt cache reads, holds for those turns; trimmed to the limit itself, it moved after every turn, and every turn ran a summarize. Each trim adds a paragraph to the summary; it keeps its newest 4,000 characters (session.MaxSummaryRunes), so it stays within the memory budget of the prompt.memory/archive/) for later retrieval. When every trimmed message was the current user's, the summary is also saved to their personal memory; a shared thread's summary is not, since it is of what everyone asked.Memory Flush (memory_flush.go) is separate and works inside one request: when the messages of a single agent loop exceed memory.auto_flush_tokens (default 80,000), the older messages are handed to the model to extract the facts worth keeping, which are saved to the agent's daily memory file, and the loop goes on with the system prompt, the current user message and the last 6 messages. The model is shown each turn labelled Person, Assistant or Tool, and keeps what a person said and what a tool returned; the agent's own replies can be wrong and are not facts, so one is left out or, when a person relied on it, saved behind Unverified, the agent's own earlier answer:. Without a model, the flush writes what people said and what tools returned, never the agent's replies.
Both limits count tokens the same way (estimateTextTokens): a digit or a non-Latin letter is one token, other text four characters a token. Counting bytes undercounted Cyrillic (two bytes a letter, about one token) and figures (Gemini spends a token on each digit) by about half.
Heavy memory operations are decoupled from the main execution thread via a persistent Job Queue:
Job records in the state database.internal/agent/memory_worker.go) processes these jobs sequentially.agent_notes.memory_entities and memory_relations for future causal reasoning.memory_consolidate job (sys-memory-consolidate, 03:00) removes near-duplicate chunks and merges agent_notes records that say the same thing; each merge is recorded in memory_revisions and can be undone. It does not touch Memory.md: the memory_consolidate tool folds old global records and daily files into Memory.md when an admin asks for it.fact_extract, graph_extract, graph_sync, memory_consolidate, belief_revision. auto_learn_error is no longer queued; rows left from an older version are drained as no-ops.GraphExtractHook to allow the memory package to trigger jobs in the agent package without circular dependencies.IndexFileAsync) is used to avoid system-wide locks during writes.idMap (O(1) lookup) is used to quickly retrieve specific chunks by ID.memory_chunks table and loaded into memory at start-up (warm-up). A memory-index.json left by older versions is deleted at start-up.internal/memory/embedding.go (OpenAI, Gemini, Ollama), chosen by memory.search.provider; none means keyword-only search.hybridSearch in internal/memory/store.go, Indexer.Search in internal/memory/indexer.go, and the memory_search tool in internal/agent/tool_memory_search.go. (internal/agent/rag.go selects tools and skills, not memory.)internal/agent/session_summarizer.go.internal/agent/auto_learn.go.