Skip to content

Memory

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.

What you can do with it

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.

The three channels

Memory is split into three independent channels that share one SQLite file. Each answers a different question.

ChannelHoldsTool surfaceRenders as
ProfileKey/value facts about you and your environmentmemory.profile.set/remove/list/history### profile
NotesSearchable freeform observationsmemory.notes.store/recall/forget### recalled + ### memory-index
ReflectionAutomated 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.

Memory that grows outside the prompt

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.

How memory is written and recalled

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:

  1. Recall happens once per turn, before the step loop. New notes written mid-turn won’t appear until the next turn.
  2. Reflection is never awaited. It runs in the background on a separate llama-server slot so it can’t disturb the main agent’s KV-cache. A new reflection aborts the previous one for the same session.

Working with profile facts

Profile facts are key/value metadata that render straight into the prompt. They’re the agent’s “always-on” knowledge about you.

Terminal window
# 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 contextual
memory.profile.history { key } # bi-temporal version chain
memory.profile.remove { key }

There are two flavors:

  • Pinned facts (pinned: true, the default) always render. Use for identity-level truth: your name, OS, primary language.
  • Contextual facts (pinned: false + keywords) only render when your message contains one of their keywords. This keeps cold facts reachable without bloating every prompt.

Working with notes

Notes are freeform text, full-text indexed (FTS5), and recalled by relevance.

Terminal window
memory.notes.store { content, tags?, scope?, workingDir? } # max 4000 chars, 8 tags
memory.notes.recall { query? | id?, k?, scope?, workingDir?, tags? }
memory.notes.forget { id }

The agent recalls notes two ways:

  • Automatic injection — before each turn, the top-K notes matching your message are rendered into ### recalled (default k = memory.recallInjection.k, 3).
  • Explicit recall — the model can call memory.notes.recall with its own query to pull more, or look up a specific note by id.

Storage stays bounded

Memory can’t grow forever. Atomic Agent enforces caps at multiple layers:

  • Per-reflection caps: at most memory.reflection.maxFactsPerCall facts (default 3) and memory.reflection.maxNotesPerCall notes (default 2) per turn.
  • Hard note cap: memory.notes.maxEntries rows (default ~1000). On overflow, the oldest rows are evicted in a single SQL statement.
  • Utility-weighted eviction (when enabled): instead of pure FIFO, rows are demoted by vote_score, recall_count, last_recalled_at, then updated_at — so downvoted, never-recalled, stale notes go first.

The memory-v2 fabric

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.

The phases, and what each adds
  • Phase 1A — dedup + eviction: FTS5 candidate fetch + Jaccard similarity merges near-duplicate notes (memory.dedup.enabled, threshold memory.dedup.fts5Threshold default 0.85). Utility-weighted eviction (memory.eviction.utilityWeighted).
  • Phase 1B — embeddings: a second llama daemon produces vectors for hybrid recall (memory.embeddings.enabled). Brute-force cosine up to ~200 rows.
  • Phase 2 — link graph: LinkGeneratorRunner writes typed edges between related notes; recall expands neighbors via BFS (memory.links.enabled, memory.links.depth, memory.links.maxExpanded).
  • Phase 5 — lessons: the cold-path 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).
  • Phase 7a — voting: up/downvotes curate ranking, clamped per item (memory.voting.maxVotePerItem) and decayed each consolidator tick (memory.voting.signalDecay, default 0.95).
  • Phase 7b — procedures: the same clusters yield advisory Procedures (2–8 plain-text steps). They’re searchable but never auto-executed (memory.procedures.enabled).

Distilled lessons and procedures

Lessons and procedures both render into the prompt tail (### lessons, ### procedures) and are recalled with their own tools:

Terminal window
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.

Configuration

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 }
}
}

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.

Inspecting memory

The whole fabric is a single SQLite file you own:

<stateDir>/memory.sqlite

It holds the profile_facts, memories (+ memories_fts), memory_embeddings, memory_links, lessons, procedures, and vote_events tables. To browse it live, open the TUI:

Terminal window
atomic-agent tui
# → Manage → Memory tab: history, dedup view, graph expansion

Gotchas worth knowing

  • Recall is per-turn, not per-step. Memory is fetched once before the step loop; mid-turn writes appear next turn.
  • Reflection runs on a separate slot. It never touches the main agent’s KV-cache, and a new reflection aborts the prior one for the same session.
  • Dedup is strict. The existing row must be a tag superset of the new entry to merge (Jaccard ≥ threshold). Otherwise both are kept.
  • Embeddings are fire-and-forget on store(). Use storeAsync() if you need the vector on disk before the next recall.
  • Migrations are idempotent and one-way. Re-running on a current-version database is a no-op; downgrades are refused.

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.