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. Nothing leaves your computer.
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 optional embeddings, link graphs, and vote curation. 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"] -->|k-means cluster| 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?, scope?, workingDir? } # max 4000 chars, 8 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.
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.LinkGeneratorRunner writes typed edges between related notes; recall expands neighbors via BFS (memory.links.enabled, memory.links.depth, memory.links.maxExpanded).ConsolidatorJob clusters linked episodes (k-means), 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.
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 } }}{ "memory": { "dedup": { "enabled": true, "fts5Threshold": 0.85 }, "embeddings": { "enabled": true }, "links": { "enabled": true, "depth": 1, "maxExpanded": 8 }, "lessons": { "enabled": true }, "procedures": { "enabled": true }, "consolidation": { "enabled": true }, "voting": { "enabled": true, "signalDecay": 0.95 } }}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.