Memory is provided by the
memoryplugin. It is OPT-IN and off by default: install and enable it first (see below). While it is off, chat runs normally with no recall and no memory growth.
localm can keep a small store of durable facts about you (your name, role, projects, tools, stable preferences) and quietly recall the relevant ones in later chats, so you do not repeat yourself every session. Everything stays on your machine: a plain JSONL store, no cloud, no database.
Memory has two halves:
- Recall injects the most relevant remembered facts into the system prompt on each chat turn, server-side.
- Consolidation distils new durable facts out of your recent sessions and folds them into the store, unattended, so memory grows on its own.
localm plugin install memory # installs and enables the memory pluginOr install it from the GUI Plugins page. The memory plugin has no extra
Python dependency to install. It adds no tab of its own: it contributes the
/api/memory* routes plus the chat recall and consolidation hooks, and its
settings render under Settings > Privacy & data, not on the Plugins page.
Memory only grows in log or full session mode. The default mode is
privacy, which writes nothing and, by default, recalls nothing (see
Privacy below). To actually accumulate a
memory, set the mode to log or full (Settings > Privacy in the app, --mode log on localm gui / serve, or the LOCALM_MODE environment variable). See
privacy.md for the full mode reference.
Semantic (meaning-based) recall also needs a small on-device embedding model:
localm setup-embeddings # installs bge-small-en-v1.5 for semantic recallWithout it, recall still works but is lexical only (exact-word BM25). See How recall ranks.
localm memory manages the same store the app does, including the corrections
that a scheduled consolidation (localm job add --memory) produces. The command
reads and writes the on-disk store directly, so it works even when the memory
plugin is not installed or enabled - installing the plugin only turns on the
automatic chat-turn recall and consolidation hooks described below.
localm memory list [--all] [--json] # what localm has remembered about you
localm memory show ID # one fact in full, with its provenance
localm memory add "..." [--kind K] [--importance F] # save a fact yourself
localm memory forget ID [-y] # delete one fact (not recoverable)
localm memory forgotten # facts localm archived on its own
localm memory restore ID # bring an archived fact back
localm memory corrections [--json] # proposals waiting for your review
localm memory accept ID | reject ID # resolve one proposal
localm memory clear [-y] # erase everything, archive includedmemory add produces the same kind of record POST /api/memory/append does:
--kind is semantic (a standing fact, the default) or episodic (something
that happened), and --importance (default 0.8) is the weight recall and
prune use. Source is always user - there is no --source flag - and
--kind accepts only those two values; anything else is a usage error. add
refuses an empty fact and refuses once the store holds 256 records.
In privacy mode add, restore, accept and reject refuse, and corrections
lists nothing (listing prunes the proposal file, which is a write); list,
show, forgotten, forget and clear still work. A command that finds
another localm process mid-write on the store waits briefly, then reports who
holds it and changes nothing.
forget and forgotten are not two halves of one thing. forget deletes
outright; the archive forgotten lists is filled only when localm drops a fact
itself - evicting one at the record cap, or replacing one you accepted a
correction for - and restore can only reach those.
clear erases everything: live facts, the forgotten archive, and any pending
or dismissed corrections, so no fact text survives it anywhere on disk. It
reads its own result back afterward and refuses to report success if anything
is still there.
Each record is a short, distilled memory, never a raw transcript:
- Semantic facts and preferences ("Prefers TypeScript", "Works on a printer firmware project").
- Episodic one-line summaries of past sessions ("Debugged a flaky upload test"), so chat can recall what you worked on, not only standing facts.
A record also carries a source that governs how much it is trusted:
- user - you typed it (via
/rememberor the memory editor). Trusted; can reach full importance. - synth - the model distilled it from a session. Importance-capped (at 0.85), and a synth fact may never overwrite or delete a user-typed one without your approval (see How memory grows below).
- import - migrated from the legacy flat
chat-memory.mdfile (imported once, inlog/fullmode).
The store lives under <data dir>/memory/chat/ as a JSONL file, with an
optional aligned vector sidecar when embeddings are present. It is owner-scoped:
localm is single-user, so all chat turns share one memory namespace.
In the GUI:
/remember <fact>adds a fact to the store./memoryopens the memory manager: view and edit your facts (one per line; Save replaces the list), click Synthesize now to distil facts from your recent chats immediately, and review any suggested corrections (see below) - nothing about them changes until you accept or reject each one.- The Memory checkbox in the parameters drawer turns recall on and off (the
memory_enabledsetting).
Over the HTTP API (the routes the memory plugin mounts; see server-api.md):
| Route | Purpose |
|---|---|
GET /api/memory |
The current facts (text + per-item metadata) and whether writes are allowed. |
PUT /api/memory |
Bulk edit. A line matching an existing fact keeps that record; new lines are added; omitted lines are deleted. |
POST /api/memory/append |
Add one fact. |
PATCH /api/memory/{id} |
Edit one record's text or importance. |
DELETE /api/memory/{id} |
Delete one record. |
POST /api/memory/consolidate |
Distil durable facts from recent sessions now (needs a loaded model). |
GET /api/memory/forgotten |
List archived (evicted or superseded) records. |
POST /api/memory/forgotten/{id}/restore |
Recover one archived record back into the store. |
POST /api/memory/corrections/{id}/accept |
Apply a suggested correction (see below). |
POST /api/memory/corrections/{id}/reject |
Dismiss a suggested correction; keep the fact as-is. |
Writes are refused with 403 in privacy mode, and with 409 when another
localm process is mid-write on the same store (nothing is changed - let it
finish and retry; the terminal commands report the same thing in words). They
are refused with 413 when an append would pass the 256-record cap, or a bulk
edit carries more than 256 lines or over 64,000 characters. There is no memory
search or import (localm memory list --json dumps the records), and the
per-item PATCH route has no GUI or CLI surface; the DELETE, forgotten and
restore routes have no GUI surface either (the terminal has forget,
forgotten and restore). Bulk editing goes through the /memory textarea, and
moving a store between machines means copying the data directory.
Consolidation reads your recent session logs and folds durable facts into the store with an ADD / UPDATE / DELETE / NO_OP loop, plus one episodic summary per new session. It runs OUT OF BAND, never in the chat hot path, and needs a loaded model. There are three ways it fires:
-
Automatically after a chat turn (the default). A debounced background pass runs at most once every 15 minutes (override with the
LOCALM_MEMORY_AUTO_INTERVALenvironment variable, in seconds). Controlled by thememory_auto_consolidatesetting - and, since this pass fires from the same chat-turn hook as recall, it also stays off whilememory_enabled(the Memory checkbox) is off, even withmemory_auto_consolidateleft on. This is what makes memory grow with no manual step: chat normally, and durable facts accumulate on their own. -
The "Synthesize now" button in the
/memorymanager, orPOST /api/memory/consolidate, for immediate results. -
A scheduled memory job (needs the
jobsplugin):localm job add distil-memory --memory --every 21600 # every 6 hoursSee jobs.md.
Consolidation asks the model for a small JSON object and parses it best-effort (small local models are not airtight, so a chatty or fenced reply is still recovered). It is guarded against the failure modes a small local model brings: session text is treated as DATA, never as instructions to follow; a synth candidate can never silently rewrite or delete a user-typed or imported fact; an unsure UPDATE keeps both facts rather than risk losing a true one; near-duplicates collapse without a model call. A pass reads about 8,000 characters of the newest session logs and considers at most 12 candidate facts. Episodic capture is watermarked, so a session is summarised exactly once and re-running consolidation adds nothing new; only a session untouched for 15 minutes is summarised, at most 5 sessions per pass, and a session with fewer than 3 distinct content words in your messages gets no summary.
When a high-confidence candidate would update or delete a user-typed/imported
fact, consolidation does not apply it - it stores a pending correction
instead and leaves the trusted fact untouched. These surface in the /memory
manager as "Suggested corrections", each with the old text struck through and
the proposed change beside it; you Apply or Keep as is per suggestion,
and accepting one archives the old value to the recoverable forgotten sidecar
before applying the change. A rejected suggestion is remembered so the same
contradiction is not re-proposed on the next pass.
In privacy mode, consolidation returns "skipped" and never calls the model.
On each chat turn the memory plugin builds a recall query from your most recent
message plus a short window of prior user turns (so an anaphoric follow-up like
"yes, do that" still carries the earlier topic), ranks the store, and injects the
top matches into the system message. Messages the client marks with origin
(see server-api.md) are left out of that query, because you
did not type them. Web search results, page reads and the GUI's notes to the
model are marked origin: "tool", so a fetched page's wording does not pull in
unrelated facts. The summarise prompt the GUI sends when it compacts a long
chat is marked origin: "client", so compaction neither recalls nor reinforces
memories. Recalled facts are neutralised and wrapped in a fenced, labelled
block marked as data, not instructions, so a memory that reads like a command
is treated as context.
- Ranking blends relevance (0.5), recency (0.3), and importance (0.2). Recency decays with a 30-day time constant for synthesized memories; user-typed and imported facts do not decay. Up to 6 facts are injected, within a 1,200-character block. The coder agent's requests get no recall.
- Recall is relevance-gated: a fact is injected only when it actually relates to the query (it shares a content word, or, with embeddings, its cosine similarity is at least 0.55; once some fact shares a word, a fact matching only by similarity needs 0.75). An off-topic turn injects nothing rather than padding the prompt with the top few facts regardless. When the semantic signal is unavailable and nothing matched, a first-person question ("what is my name?") brings in at most 2 user-typed or imported facts.
- Semantic vs lexical. With
localm setup-embeddingsinstalled, recall uses meaning-based cosine similarity (a paraphrase still matches); without it, recall falls back to exact-word BM25. Facts stored before you installed the embedding model are re-embedded in the background (a sweep at most once an hour,LOCALM_MEMORY_BACKFILL_INTERVALin seconds, plus up to 64 records per consolidation pass), so semantic recall turns on retroactively.
When memory injects facts, the server reports it so you are never guessing:
- The GUI shows a "used N memories" chip on the reply; click it to open the memory manager.
POST /v1/chat/completionscarries anX-Localm-Memoryresponse header with the count, the injected items, and a degrade reason when the semantic signal could not be used:no_embedder(no embedding model resolved - you have not runlocalm setup-embeddingsyet),no_vectors(records not embedded yet),low_coverage(too few records carry a vector),dim_mismatch(the vector sidecar mixes dimensions, usually from switching embedding models), orquery_embed_failed(the embedder itself failed on this turn's query). Any of these means recall fell back to lexical BM25 for that turn.
A namespace holds at most 256 records, each capped at 500 characters. Low-value
synthesized memories decay over time and are pruned. User-typed and imported
facts are never dropped by decay; only the size cap can evict them, weakest
first, and every eviction is archived to a recoverable .forgotten.jsonl
sidecar next to the store, never silently hard-deleted. GET /api/memory/forgotten lists what is archived and POST /api/memory/forgotten/{id}/restore recovers one record back into the live
store.
Memory treats privacy mode more strictly than the rest of the app. The default
mode is privacy, and in it memory is fully inert: no facts are written, and
none are recalled, so nothing from a past session reaches the model.
| Mode | Recall | Growth |
|---|---|---|
| privacy (default) | Off, unless you opt into read-only recall | Off (no writes) |
| log / full | On (while memory_enabled and the Memory checkbox are on) |
On (while memory_auto_consolidate is on; the automatic pass also needs memory_enabled - manual Synthesize now and a scheduled memory job are unaffected by it) |
You can opt into recalling your existing memories during privacy-mode chats
without ever writing new ones: turn on Allow memory recall in privacy mode
(memory_recall_in_privacy, with the per-surface ..._chat switch). Even then,
it only READS: no reinforcement, no consolidation, no migration.
Because the default is privacy, memory does nothing out of the box until you
switch to log or full. This is deliberate (offline-first, no traces by
default), not a bug.
If you run the MCP server, an external client can read your memory
through the memory_recall tool (and write to it through memory_append if
you separately opted in with --memory-write). Both tools are refused
whenever the relevant session is in privacy mode, the same guarantee above;
pass --no-memory to hide memory_recall entirely.
The coder agent keeps a separate store of past-session lessons (what worked, what
failed) per project, recalled at the start of a coder session. It follows the
same privacy contract (lessons are only written in log/full mode) and is
distinct from chat memory described here. See cli.md for the coder.
- privacy.md - session modes and the no-traces contract.
- gui.md - the chat GUI, slash commands, and the memory manager.
- jobs.md - scheduling a memory-synthesis job.
- server-api.md - the
/api/memory*routes and theX-Localm-Memoryheader. - mcp.md -
memory_recall/memory_appendas MCP tools. - rag.md - Knowledge collections, the other way to ground replies in your own content.