Store facts about you
Pin durable facts (memory.profile.set) like your name, OS, or preferred language. They render into every relevant prompt automatically.
Most agents “remember” by stuffing the entire chat history back into the prompt. That gets expensive fast and crowds out the model’s attention. Atomic Agent does the opposite: it remembers in a local SQLite database and feeds the model only the few facts that matter for the turn in front of it.
The result is an agent that recalls your name, your project conventions, and lessons it learned last week — without dragging a giant transcript along for the ride.
Store facts about you
Pin durable facts (memory.profile.set) like your name, OS, or preferred language. They render into every relevant prompt automatically.
Save and search notes
Drop freeform notes (memory.notes.store) and recall them later by keyword search (memory.notes.recall). Full-text indexed, no setup.
Let it learn on its own
After each turn, a background reflection pass quietly extracts facts and notes from the conversation — you don’t have to ask.
Keep everything local
Memory lives in <stateDir>/memory.sqlite on your machine and stays there. It travels only if you deliberately point embeddings at a remote provider.
Memory is split into three independent channels that share one SQLite file. Each answers a different question.
| Channel | Holds | Tool surface | Renders as |
|---|---|---|---|
| Profile | Key/value facts about you and your environment | memory.profile.set/remove/list/history | ### profile |
| Notes | Searchable freeform observations | memory.notes.store/recall/forget | ### recalled + ### memory-index |
| Reflection | Automated end-of-turn extraction | (runs itself) | feeds the other two |
On top of these, the memory-v2 fabric adds distilled lessons and advisory procedures (memory.lessons.recall, memory.procedures.recall) plus link graphs and vote curation — all enabled by default — and optional embeddings. More on those below.
This is the core idea. The prompt the model sees is small and bounded. Memory grows on disk.
Each turn, Atomic Agent pulls only what’s relevant from SQLite and renders it into the variable tail of the prompt — never the cached stable prefix. The tail sections each have a hard token cap:
### profile — pinned facts + facts whose keywords hit your message (cap: memory.profile.maxTokens, default 512)### recalled — top-K full note bodies matching your message (cap: memory.recallInjection.maxTokens, default 400)### memory-index — compact pointers to other notes by id (cap: memory.index.maxTokens, default 300)### lessons — distilled patterns (cap: memory.lessons.maxTokens, default 300)### procedures — advisory how-to templates (cap: memory.procedures.maxTokens, default 400)Because these always live in the tail and never the prefix, your KV-cache survives. Memory injection costs a few hundred tokens, not a full transcript.
Every turn touches memory twice: a hot read before the model runs, and a fire-and-forget write after it replies.
graph TD
A["Turn starts<br/>runTurn(userMessage)"] -->|abortPending| B["Cancel any in-flight reflection"]
B --> C["MemoryContextProvider<br/>.buildMemoryContext()"]
subgraph Recall["Hot read (per-turn, before model)"]
C -->|recallHybridAsync| D["MemoryStore<br/>BM25 over memories_fts"]
D -.optional cosine.-> E["EmbeddingStore"]
C -->|expand| F["LinkStore.expand()<br/>BFS neighbors"]
C -->|recall| G["LessonStore + ProcedureStore"]
C -->|get active| H["ProfileStore<br/>superseded_by IS NULL"]
end
D --> I["Render variable tail:<br/>### profile / ### recalled<br/>### memory-index<br/>### lessons / ### procedures"]
F --> I
G --> I
H --> I
I --> J["Agent loop runs steps<br/>→ assistant_reply"]
subgraph Write["Hot write (post-turn, async, never awaited)"]
J -->|fire-and-forget| K["ReflectionRunner<br/>on dedicated llama slot"]
K -->|micro-prompt| L["Extract SET / NOTE / EVOLVE"]
L -->|SET| M["ProfileStore.set()"]
L -->|NOTE| N["MemoryStore.storeAsync()<br/>dedup → insert/merge"]
N -->|overflow| O["FIFO / utility-weighted eviction"]
N -.fire-forget.-> E
K -.phase 2.-> P["LinkGeneratorRunner"]
K -.phase 7a.-> Q["VoteRunner"]
end
subgraph Cold["Cold path (periodic consolidator)"]
R["ConsolidatorJob<br/>every ~6h"] -->|connected components<br/>over link graph| S["Distill cluster → Lesson + Procedure"]
S -->|archive parents| T["memories.consolidated_into"]
S -->|decay| Q
end
style C fill:#e1f5ff
style K fill:#fff3e0
style R fill:#f3e5f5
style I fill:#e8f5e9
Two things to notice:
Profile facts are key/value metadata that render straight into the prompt. They’re the agent’s “always-on” knowledge about you.
# The agent calls these tools itself, but here's the shape:memory.profile.set { key, value, pinned?, keywords? }memory.profile.list # shows * for pinned, ~ for contextualmemory.profile.history { key } # bi-temporal version chainmemory.profile.remove { key }There are two flavors:
pinned: true, the default) always render. Use for identity-level truth: your name, OS, primary language.pinned: false + keywords) only render when your message contains one of their keywords. This keeps cold facts reachable without bloating every prompt.Notes are freeform text, full-text indexed (FTS5), and recalled by relevance.
memory.notes.store { content, tags? } # max 4000 chars, up to 16 tagsmemory.notes.recall { query? | id?, k?, scope?, workingDir?, tags? }memory.notes.forget { id }The agent recalls notes two ways:
### recalled (default k = memory.recallInjection.k, 3).memory.notes.recall with its own query to pull more, or look up a specific note by id.Memory can’t grow forever. Atomic Agent enforces caps at multiple layers:
memory.reflection.maxFactsPerCall facts (default 3) and memory.reflection.maxNotesPerCall notes (default 2) per turn.memory.notes.maxEntries rows (default ~1000). On overflow, the oldest rows are evicted in a single SQL statement.vote_score, recall_count, last_recalled_at, then updated_at — so downvoted, never-recalled, stale notes go first.Beyond facts and notes, Atomic Agent layers a richer fabric inspired by Complementary Learning Systems — a fast “hippocampal” write path and a slow “neocortical” consolidation path.
Everything below is on by default except phase 1B (embeddings).
memory.dedup.enabled, threshold memory.dedup.fts5Threshold default 0.85). Utility-weighted eviction (memory.eviction.utilityWeighted).memory.embeddings.enabled). Brute-force cosine up to ~200 rows. The one phase that’s off by default — it needs the embedding daemon running.LinkGeneratorRunner writes typed edges between related notes; recall expands neighbors via BFS (memory.links.enabled, memory.links.expansionDepth default 1, memory.links.maxExpanded default 12).memory.evolution.enabled). Note content stays append-only regardless.ConsolidatorJob clusters linked episodes, distills each cluster into one Lesson (a 1–3 sentence principle), and archives the parents (memory.lessons.enabled, memory.consolidation.enabled).memory.voting.maxVotePerItem) and decayed each consolidator tick (memory.voting.signalDecay, default 0.95).memory.procedures.enabled).Lessons and procedures both render into the prompt tail (### lessons, ### procedures) and are recalled with their own tools:
memory.lessons.recall { query?, id?, k? }memory.procedures.recall { query?, id?, k? }Adding lessons (phase 5) and procedures (phase 7b) are the only two changes the memory fabric makes to the byte-stable prompt prefix. Each one invalidates the KV-cache once at rollout, so plan a fresh session pool when you first enable them.
Raw user messages make poor search queries. “What did we decide about that?” has almost no keywords for BM25 to bite on, so recall comes back empty exactly when context would help most.
The v2.5 query rewriter sits in front of recall and fixes this. It’s enabled by default (memory.retrieve.rewriter.enabled) and gated by a heuristic (gateMode: "heuristic"), so it only fires when a message actually looks like it needs rewriting — a short or referential query gets expanded using the last few turns, while a keyword-rich one goes straight through untouched.
{ "memory": { "retrieve": { "rewriter": { "enabled": true, "gateMode": "heuristic", "historyTurns": 3, "timeoutMs": 3000 } } }}Two design choices keep it cheap. The rewriter runs on slotId: -1, a scratch slot, so it never disturbs the main agent’s KV-cache or the reflection slot. And it’s bounded by timeoutMs (default 3000) — if the rewrite doesn’t come back in time, recall proceeds with the original query rather than stalling the turn.
Memory is tuned entirely under the memory.* block in <stateDir>/config.json. The most common knobs:
{ "memory": { "profile": { "enabled": true, "maxTokens": 512, "contextualKeywordGate": true }, "notes": { "enabled": true, "maxEntries": 1000, "recallDefaultK": 5 }, "recallInjection": { "enabled": true, "k": 3, "maxTokens": 400 }, "index": { "enabled": true, "limit": 20, "maxTokens": 300 }, "reflection": { "enabled": true, "maxFactsPerCall": 3, "maxNotesPerCall": 2 } }}These are the defaults, shown explicitly — only embeddings.enabled differs from a stock install, where it is false.
{ "memory": { "dedup": { "enabled": true, "fts5Threshold": 0.85 }, "embeddings": { "enabled": false }, "links": { "enabled": true, "expansionDepth": 1, "maxExpanded": 12 }, "evolution": { "enabled": true }, "lessons": { "enabled": true }, "procedures": { "enabled": true }, "consolidation": { "enabled": true, "minClusterSize": 3, "requireSharedTag": true }, "voting": { "enabled": true, "signalDecay": 0.95 }, "retrieve": { "rewriter": { "enabled": true, "gateMode": "heuristic" } } }}The link-graph key is expansionDepth, not depth — a config using depth silently falls back to the default rather than erroring.
If memory.notes.enabled is false, the MemoryContextProvider is never built and the agent skips all memory sections in the prompt — a graceful, zero-overhead degrade.
The whole fabric is a single SQLite file you own:
<stateDir>/memory.sqliteIt holds the profile_facts, memories (+ memories_fts), memory_embeddings, memory_links, lessons, procedures, and vote_events tables. To browse it live, open the TUI:
atomic-agent tui# → Manage → Memory tab: history, dedup view, graph expansionstore(). Use storeAsync() if you need the vector on disk before the next recall.Prompt system
How the variable tail is budgeted and why memory sections never touch the stable prefix.
Memory tools
Full reference for memory.profile.*, memory.notes.*, memory.lessons.recall, memory.procedures.recall.
Configuration
Every memory.* config key, with defaults and trade-offs.
Local models
The chat + embedding daemon pair that powers hybrid recall.