Engram Documentation
Install
# the package (Python 3.11+)
pip install engram-proxy
# or run without installing:
uvx --from engram-proxy engram up
# the local models Engram uses for background work (~2 GB total)
ollama pull qwen3:1.7b qwen3-embedding:0.6b
Requirements: Ollama running locally (for retrieval embeddings and background summarization - required even when your chat model is in the cloud), Python 3.11+. Tested on macOS Apple Silicon; Linux should work identically.
Quickstart
engram up # proxy on :11435, forwarding to Ollama on :11434
# in another terminal: same ollama CLI, new port - that's the whole integration
export OLLAMA_HOST=127.0.0.1:11435
ollama run qwen3:1.7b
Tell the model a fact. Quit. Start a new session and ask about it obliquely - the answer can only come from memory. Watch what happened with engram why, or open the live brain view at 127.0.0.1:11435/engram/ui.
Memories form a few seconds after each exchange (background summarization waits for GPU idle), so a fact becomes recallable in the next session, not the same instant you say it.
Integrations
Stock Ollama CLI / any OpenAI-compatible client
export OLLAMA_HOST=127.0.0.1:11435 # ollama CLI
base_url = "http://127.0.0.1:11435/v1" # OpenAI SDKs, Hermes Agent, etc.
OpenClaw
# OpenClaw mandates Ollama's native API - Engram speaks it byte-faithfully
{ "models": { "providers": {
"engram": { "baseUrl": "http://127.0.0.1:11435", "api": "ollama" }
} } }
Anthropic-protocol clients (local memory for cloud models)
ENGRAM_ANTHROPIC_UPSTREAM=https://api.minimax.io engram up
# point the client's anthropic base_url at:
http://127.0.0.1:11435/anthropic
Auth headers pass through untouched. The whole Messages surface follows the cloud upstream (count_tokens, batches, prefixed /v1/models included).
CLI reference
| command | what it does |
|---|---|
| engram up | run the proxy daemon (also serves the observatory at /engram/ui) |
| engram list [-n N] | recent traces: title, gist, access weight, origin |
| engram recall "query" | dry-run recall: per-candidate scores, admission verdicts, the block that would inject. Never mutates the store. |
| engram why | explain the last real injection, score by score |
| engram profile | show the consolidated user profile |
| engram stats | store counts, queue depth, degraded-recall counter |
| engram bench [-n N] | measure embed + recall latency on your machine |
| engram lme --n 100 | LongMemEval-S subset harness: per-question fresh store, deterministic retrieval scoring, configurable judge (see BENCHMARKS.md). --watch PORT serves a live progress dashboard while it runs |
| engram eval [--model M] | the built-in memory regression scenario: seeds a throwaway store over a scripted multi-session conversation, then scores retrieval and answers on 10 probes |
| engram lapse 72h | advance the decay clock (testing/demos) - accepts 30m / 72h / 45d |
| engram import file.md | bootstrap memories from a notes file (e.g. an agent's MEMORY.md), backdated so they neither flood recall nor arrive decayed |
| engram mcp | run the memory-verbs MCP server on stdio |
HTTP endpoints
| route | behavior |
|---|---|
| POST /api/chat | Ollama-native NDJSON, memory-enabled |
| POST /v1/chat/completions | OpenAI-compatible SSE, memory-enabled |
| POST [/prefix]/v1/messages | Anthropic Messages (streams SSE), memory-enabled; routes to ENGRAM_ANTHROPIC_UPSTREAM when set |
| /api/generate, /api/tags, /api/show, /v1/models, embeddings… | byte passthrough (raw completions are never injected - there is no safe injection point in a FIM prompt) |
| GET /engram/ui | the observatory (live brain view) |
| GET /engram/memories | JSON trace snapshot with live activation |
| GET /engram/why · /engram/stats | last injection explained · store health |
Per-request headers: X-Engram: off bypasses memory; X-Engram-Session: id pins session identity.
Configuration
Everything is environment variables; defaults are field-tested.
| variable | default | meaning |
|---|---|---|
| ENGRAM_PORT | 11435 | proxy listen port |
| ENGRAM_UPSTREAM | http://127.0.0.1:11434 | chat forward target (OpenAI/Ollama routes) |
| ENGRAM_ANTHROPIC_UPSTREAM | (empty) | where /v1/messages traffic goes - set to a cloud endpoint to give a cloud model memory |
| ENGRAM_OPENAI_UPSTREAM | (empty) | where /v1/chat/completions goes - any OpenAI-compatible provider (OpenRouter, DeepSeek, ...). /v1/models follows it; /v1/embeddings and Ollama-native /api/chat always stay local |
| ENGRAM_SERVICES | http://127.0.0.1:11434 | local Ollama for embeddings, summarization, /api/ps - background work never leaves this |
| ENGRAM_DB | ~/.engram/engram.db | the store. Back it up with sqlite3 … ".backup out.db" (WAL mode makes bare cp copies stale) |
| ENGRAM_EMBED_MODEL | qwen3-embedding:0.6b | retrieval embedder. nomic-embed-text is ~3× faster but measurably worse at telling related from unrelated cues |
| ENGRAM_SUMMARIZER_MODEL | qwen3:1.7b | gist writer (fact-dense: every stated name, number, and date survives as its own clause). Sub-1B models destroy facts and are unsupported; a missing model falls back to extractive summaries |
| ENGRAM_PROFILE_MODEL | (summarizer) | model for profile consolidation |
| ENGRAM_QUARANTINE | (empty) | comma-separated source labels never injected, e.g. observed (all third-party) or observed:Ops Team |
| ENGRAM_EXPAND_MIN | 1000 | token-budget floor for evidence expansion (verbatim episode quotes under each gist on big tiers); 0 disables |
| ENGRAM_ANN_TRACES | 50000 | store size at which dense search switches from exact to the IVF index (pure numpy, recall parity 1.000 on clustered vectors, benchmarked). Below it there is nothing to tune |
Memory verbs (MCP)
Agents that can call tools get explicit memory verbs over the same store as the transparent proxy. Register engram mcp as a stdio MCP server in your agent framework.
| tool | teaches the agent to… |
|---|---|
| memory_search | always check memory before answering about the user's past; include_silent shows superseded/forgotten history |
| memory_save | persist a stated fact instantly (no waiting for background formation) |
| memory_correct | fix a wrong/outdated memory - supersedes (history kept, never re-injected), never overwrites |
| memory_edit | reword a garbled gist in place (for changed facts, use correct instead) |
| memory_pin | mark permanently important: outranks everything, never decays, still supersedable |
| memory_forget | silence (reversible) or, with hard=true, delete for privacy |
| memory_profile | read (or refresh) the consolidated user profile |
| memory_recent · memory_stats | newest traces · store health |
Architecture
client ──► engramd :11435 ──► upstream (Ollama :11434 / cloud)
│ recall: hybrid search + ACT-R activation (~90ms, no LLM)
│ inject: budgeted block, placement preserves prompt caches
│ tee: response streams byte-verbatim; shadow copy → episode log
└─ write-behind (GPU-idle only): summarize → embed → link → store
└── engram.db (SQLite WAL: traces, episodes, links,
schemas, FTS5, vectors, job queue)
- Recall is pure arithmetic. One capped embedder call at most; hard deadline; any failure = uninjected passthrough. Memory is an enhancement, never a gate.
- Admission before ranking. A trace must clear raw relevance thresholds (cosine, or literal token matches) before activation orders anything - so "inject nothing" is the default on unrelated turns, which is what protects small models from distractors.
- Adaptive budgets. The upstream model is probed once: <4B params → 2 gists / 350 tokens; 4–14B → 4 / 1000; ≥14B and cloud → 6 / 2000.
- Cache-preserving injection. Prompts over ~4k tokens get the block on the newest user message, leaving the cached prefix byte-identical - measured 90s → 2s per turn locally, and it preserves cloud providers' cache-read discounts.
- Evidence expansion. The gist is a lossy index over the lossless episode record. On big-budget tiers (1000+ tokens), each rendered gist reaches back into its source episode and quotes the most cue-relevant verbatim sentences, strictly from leftover budget, user's own words only, secrets and third-party text never travel. Measured on LongMemEval, full 500: +18 frontier-model answers (62.8% to 66.4%) over the same blocks without it, concentrated in multi-session and knowledge-update questions. Disable with
ENGRAM_EXPAND_MIN=0. - Scales past personal stores. Below 50k memories dense search is a single exact matmul. Past it an IVF index (spherical k-means, pure numpy) bounds every query to a sixteenth of the store, lexical admission prunes tokens present in >2% of traces, and index rebuilds run on a background thread only when something changed - a rebuild that a write raced is discarded, never installed. Measured: 80ms p95 full recall at 100k, 85ms at 250k - still embed-bound. See BENCHMARKS.md.
Memory lifecycle
- Formation is picky. Pure questions, smalltalk, and framework plumbing never become memories. Statements are summarized with user-fact primacy (exact names and values; assistant elaboration ignored) with a deterministic extractive fallback.
- Use strengthens. Retrieval counts as re-presentation (the testing effect) at loop-safe weights, capped per session.
- Corrections supersede. Update markers ("actually…", "changed to…") or conflicting values silence the stale trace and keep it on record.
- Consolidation. During idle, user-stated memories distill into a profile schema (aggregate questions like "what do you know about me" inject it) and into topic schemas: clusters of related memories get one synthesized summary each, injected when a cue lands near the cluster. Both are single-level by design - schemas build from traces, never from previous schemas, so no compounding drift.
- Forgetting is retrieval failure. Unpinned traces below the activation line past 30 days drift to silent - invisible to injection but never gone. A strong direct cue (cosine ≥ 0.45 when nothing active answers) resurrects a silent memory automatically, and
memory_search include_silentreaches them explicitly. A lone unreinforced mention lives ~46 days. - The graph primes. Traces link temporally (6h co-allocation window), semantically, and on co-retrieval; what was recalled last turn makes its neighbors easier to recall this turn, with fan damping so hubs can't dominate.
Provenance & trust
Every memory records whose words it came from:
- user - the human. Full strength, eligible for salience boosts (corrections, "important", names, deadlines).
- observed:<name> - third parties relayed by a gateway (group chats a bot observes). Half-weighted, never salience-boosted (imperatives from strangers are the grammar of injection attacks), rendered as "(reported by X, unverified)", and structurally unable to supersede anything the user said.
- system - agent-framework plumbing arriving as user-role turns. Never becomes memory.
Injected blocks are stripped before storage (Engram never re-summarizes its own output), byte-identical retries dedup at half weight, and ENGRAM_QUARANTINE can bar any source from injection entirely while staying searchable.
The observatory
Open /engram/ui: memories orbit a neural core at a radius set by their live activation - reinforced memories fall inward, decaying ones drift out, recalls flare ember, pinned traces wear a ring, silent and superseded history dims at the rim. Press / to search; click any mote for its full dossier (gist, β importance, access history, provenance). It polls the daemon every 4s and doubles as the debugging surface: if recall surprises you, look at the sky. Click the LAST RECALL readout (bottom right) to inspect the last injection in full: block, latency, mode, and scored candidates, each clickable through to its dossier. Linkable as /engram/ui#recall.
Selecting a memory keeps its hebbian web lit: ember filaments run to every linked trace while the dossier shows the gist, activation, importance, provenance, and pin state. Deep-linkable as /engram/ui#trace=<id>.
Hovering any mote shows its id, status, live activation, and title in place, and lights up its links; here the ember-ringed pinned trace with its web.
Day one and lived in. A fresh store is a lone core over empty orbits; every conversation after that grows the constellation - memories fall inward as they prove useful, links form between the ones recalled together, and the ones that stop mattering dim toward the rim instead of disappearing. The counters top right tell the story at a glance (demo data).
Cloud models
Both cloud protocols work, independently or together. Set ENGRAM_ANTHROPIC_UPSTREAM and point your client's Anthropic base_url at http://127.0.0.1:11435/anthropic, or set ENGRAM_OPENAI_UPSTREAM to any OpenAI-compatible provider (OpenRouter, DeepSeek, Groq, ...) and point the client's OpenAI base_url at http://127.0.0.1:11435/v1. Chat goes to the cloud; embeddings, summarization, and consolidation stay on local Ollama, so the only added cloud cost is the injected block (≤2000 tokens on the cloud tier, typically far less - around 1% of a real agent prompt). Your memory store never leaves your machine.
Known weaknesses
Measured on LongMemEval-S (all 500 questions; full methodology and every number in BENCHMARKS.md). Retrieval reached 98.1%: the right memories almost always make the injected block. The judged answer scores (62.8% with a frontier answering model, 33.8% with a 1.7B) are bounded by four things, ranked by measured impact:
- 1. Lossy gists under extractive summarization. The benchmark ingests with the extractive fallback (verbatim truncation: ~60 user words + ~40 assistant words), because LLM-summarizing 500 x ~50 sessions takes days on a laptop. In 21 of 37 analyzed wrong answers the needed detail was truncated inside the gist - the right memory reached the block without the fact the question wanted. Doubling the injection budget recovered nothing (59 vs 61 per 100), proving the loss happens at write time, not at selection. Production's primary path is LLM summarization, which distills instead of truncating; quantifying that gap is the next planned ablation.
- 2. The answering model's reading ability. In the other 16 of 37, the answer was in the block and the model fumbled it. Same blocks: 62.8% frontier vs 33.8% local 1.7B. Retrieval is model-independent; comprehension is not - this is the argument for the cloud routes.
- 3. User-fact primacy. Knowledge that only ever appeared in the assistant's words forms weakly, by design - imperatives and facts from anyone but the user are exactly what memory poisoning looks like, so that trade is deliberate. It caps the single-session-assistant category (21/56) and is published as a property, not fixed quietly.
- 4. Consolidation is not exercised by the benchmark. Aggregate and preference questions are what the idle-time profile and topic schemas exist for; a benchmark run never idles, so those answers rely on point traces alone.
Retrieval's own 9 misses out of 470 are taxonomized in BENCHMARKS.md too: 3 assistant-side facts, 3 relative-date cues ("last Tuesday" - embeddings cannot do date arithmetic; date-aware cue expansion is on the roadmap), 2 vague preference cues, 1 paraphrase below the admission threshold.
Troubleshooting
| symptom | cause / fix |
|---|---|
| address already in use | another engramd holds :11435 - pkill -f "engram up" |
| no memories form | check engram stats queue depth; the summarizer waits ~30s of GPU idle. Missing qwen3:1.7b → extractive summaries (still correct, terser) |
| degraded_recalls climbing | embed calls exceeding the 250ms cap (GPU contention) - recall degrades to lexical-only for those turns and recovers on its own |
| slow first turns through big agents | huge prompt prefill; subsequent turns should hit the prompt cache. If every turn is slow, something churns the prompt head upstream of Engram |
| model answers ignore injected memory | sub-2B answering models fumble context they're given; the memory block is visible in engram why - verify retrieval vs answering separately, like engram eval does |
| backup | sqlite3 ~/.engram/engram.db ".backup backup.db" - never bare cp (WAL) |
MIT licensed · source · design specification · built with a research-first process: every default in this page traces to a measurement or a field failure.