FactMem
A local memory engine any AI tool can use. You own the SQLite file. Install with `npx -y @factmem/mcp`.
Documentation
FactMem
A local memory engine any AI tool can use. GitHub gordonkjlee/factmem, npm @factmem/mcp.
Not a hosted plane. Not a vendor blob. You own the SQLite file. Formerly published as OpenMemory (@openmem/mcp); that name is Mem0's hosted "OpenMemory MCP" at mcp.mem0.ai. Not the code-index MCP abolotnov/factmem.
It records, stores, and retrieves structured knowledge. Domain routing, entity extraction, deduplication, and supersession run in the server. Exposed as an MCP server.
Quick Start
Needs Node 22.5 or 24+.
Paste this. Restart the client. The server creates ~/.factmem on first boot.
{
"mcpServers": {
"factmem": {
"command": "npx",
"args": ["-y", "@factmem/mcp@0.24.2"]
}
}
}
In the client, state something durable in ordinary conversation — there is no remember command.
That is the file you own. Transcript file (Claude Code or Cursor): next section. CLI: below.
How conversations get in
Two ways. Pick one per store.
| Copy from transcripts | The assistant records | |
|---|---|---|
| Who | Claude Code or Cursor, when that client writes a JSONL file here | Any MCP client (Grok Build, Desktop, …) |
| How | Name a source; FactMem copies new lines into your file | Empty sources; the assistant calls capture_fact |
| First run | TTY walk-through, pick copy, set cwd, then pull | The paste above (already record) |
On a copy store, capture_fact is a correction for every MCP client, not only the one that writes JSONL. Grok has no transcript adapter — do not put Claude Code on copy and Grok on the same store expecting Grok to record.
factmem init
Pick copy, set cwd, then factmem pull once. More than 50 new events: factmem consolidate. After that, the server copies new lines when it handles a call.
Compact (optional): factmem pull --flush — not a turn-end Stop hook.
Replay: factmem.dev/demo.html.
What you get
- One file you own. SQLite by default. Optional Postgres. Isolation is the directory, not a column.
- Entity graph. People, organisations, projects, places, products — extracted, typed and linked.
- Hybrid search. BM25 + structured domain + entity-graph paths, merged via Reciprocal Rank Fusion. An embedding provider adds meaning as a fourth list; off by default.
- In-session memory.
get_session_contextis the same briefing asmemory://briefing. Tools-only clients should call it at session start. - Immutable history. Facts are never deleted, only superseded.
How it works
One SQLite file you own. Three tables in that file, not three databases:
- D (
session_events) — what was said (pulled transcripts, orlog_event) - I (
session_facts) — what was just extracted, orcapture_fact - K (
facts) — graduated knowledge
FTS5 (words) and optional embeddings (meaning) are indexes of K. They are not a second store. Semantic search is off unless you turn it on: search "shellfish" finds a shellfish fact, search "food" does not, until you choose an embedding model — a model is an opinion about what “similar” means.
Two speeds. Extract turns new transcript lines into self-contained facts (session start leftover, factmem pull with a listening server, or consolidate). Flush (pull --flush, shutdown) graduates those pending facts. The MCP server copies the raw log on a call; it does not extract then. Consolidation does not invent a sentence nobody said.
Storage needs Node. Intelligence needs a language model. By default that is the Claude Code CLI on your existing subscription. Without it, consolidation falls back to a built-in heuristic that does not extract facts from transcripts. capture_fact still stores facts, with no entities and no domain routing.
MCP
Works with Claude Code, Claude Desktop, and any MCP-compatible tool. Data is stored at ~/.factmem by default. If that folder does not exist and ~/.openmemory already does, that existing store is used — it is not copied. That one directory is the whole install. To use a different path, add "env": { "FACTMEM_DATA": "/absolute/path" } to the MCP snippet. JSON accepts forward slashes on Windows. OPENMEMORY_DATA is still read.
Cursor consumes tools but not resources until a later adapter exists — search_knowledge and get_entity still work there; call get_session_context at session start.
Resources are context the client loads automatically — no tool call. Tools only help if the assistant remembers to reach for them; resources are simply present.
memory://briefing— Everything worth knowing right now: profile, what was learned in the last consolidation, open threads, and recent knowledge. Markdown, kept to roughly a screenful.memory://profile— Core identity facts, most important first.
Both are read-only views over the same database the tools query. Clients that never load resources (Cursor, Windsurf, Grok) get the same briefing by calling get_session_context at the start of a conversation. No second profile schema.
Tools
Session
log_event— Log conversation events (messages, artifacts).get_events— Retrieve events from current or previous session.get_session_context— Working briefing (the same markdown asmemory://briefing) plus facts captured in this session. Call at the start of every conversation if the client does not load resources.
Reading
get_entity— Everything known about any named subject — person, organisation, project, place, product — and how it connects. When several rows share the name under different types, facts from all of them come back. Hyphens, underscores, and stray punctuation count as the same letters only when that does not join two names already stored as separate rows. If there is no entity by that name, facts that mention the wording still come back rather than an empty miss.get_context— Everything relevant to a topic (search + entity traversal)search_knowledge— Hybrid search across graduated knowledge
Writing
capture_fact— Store a fact. On a pull store this is a correction for something extraction missed; on a store with emptysourcesit is how facts get in. The description the assistant sees is generated from that same rule.consolidate— Integrate pending facts into long-term knowledge. Extracts entities, resolves duplicates, detects contradictions, builds the knowledge graph.- Inference tools — Opt-in, off by default (
inferences.enabledin config.json). A hypothesis cites existing fact ids and stays pending until confirmed. Those tools are not registered until you turn the gate on. Consolidate never invents a sentence nobody said.
Meta
get_schemas— Available domains and structureget_stats— Fact count, entity count, domain distribution, extract backlog, intelligence spend
CLI
The MCP JSON starts the server via npx and does not need a global install. npm install -g puts factmem on PATH for init, settings, stats, and inspect. The same CLI without PATH is npx -y -p "@factmem/mcp" -- factmem — pin the version; quote the package so PowerShell does not splat. -p and -- stop an older global binary winning. npx -y @factmem/mcp with no -p / factmem is the server; do not run it as a shell command for init, settings, or stats.
These CLI commands work in bash, zsh, and PowerShell. Quote @factmem/mcp in PowerShell. Git Bash /c/... paths are not PowerShell; use C:/... and pass --data instead of cd or export. ~/ is expanded on every platform. WSL uses /mnt/c/....
npm install -g @factmem/mcp@0.24.2
factmem init --yes
npx -y -p "@factmem/mcp@0.24.2" -- factmem init --yes
npx -y -p "@factmem/mcp@0.24.2" -- factmem settings --json
npx -y -p "@factmem/mcp@0.24.2" -- factmem stats
npx -y -p "@factmem/mcp@0.24.2" -- factmem inspect
| Job | Use |
|---|---|
| MCP server (what the client starts) | The JSON snippet: npx with args -y and a pinned @factmem/mcp@…. No global install. |
factmem on PATH | npm install -g @factmem/mcp@… (same pin). Update it when you bump the snippet. |
| Change extra knobs later | factmem settings (or settings --data <dir>). Does not reset the file. |
| One CLI command, no PATH | npx -y -p "@factmem/mcp@…" -- factmem … |
factmem init [dir]
The walk-through is how a human first-run writes config.json. Skip it and the server still creates the directory on first MCP boot.
On a terminal, init asks data directory, copy transcripts vs assistant records (default copy), semantic search, and More settings. --yes never prompts and leaves sources empty. --web prints a 127.0.0.1 URL and does not open a browser; --yes refuses --web. On a terminal, --force still asks those questions, then replaces the whole file; --yes --force is the silent reset. --force does not merge with the previous file.
factmem init --yes
factmem init --yes ~/my-memory
factmem init --yes --force
The generated config.json is where you change consolidation behaviour — most notably intelligence.provider (cli by default; heuristic for a zero-dependency regex fallback, or FACTMEM_PROVIDER=heuristic at runtime). Init does not ask that field.
factmem settings
Change extra knobs on an existing config.json (CLI model, timeout, optional local extract). Does not reset the rest of the file. Refuses if there is no config.json (this command does not create a store). --json / not a terminal prints the current knobs and does not write. --web is the same knobs on a local page (print URL, no auto-open).
factmem settings
factmem settings --data ~/my-memory
factmem log-event
Inserts events directly into the database (no running server needed). Supported for demos and for stores that have no named source. Not the Claude Code or Cursor default — that is sources plus factmem pull.
# From a hook (reads JSON payload from stdin):
echo '{"hook_event_name":"UserPromptSubmit","prompt":"hello"}' | factmem log-event --role user
# With explicit content:
factmem log-event --role user --event-type message --content "hello world"
# Options:
# --role user | assistant | system | tool (default: user)
# --event-type message | tool_call | tool_result | artifact (default: message)
# --content-type text | json | image | audio | binary (default: text)
# --content Event content (or pipe via stdin)
# --speaker Named participant when the transcript has one
# --session-id Target session (default: most recent)
# --data Data directory (default: ~/.factmem or FACTMEM_DATA)
factmem pull
Ingest new session events from config.sources. Empty sources is a successful no-op:
factmem pull
factmem pull --no-tick
factmem pull --flush
# Options:
# --data Data directory (default: ~/.factmem or FACTMEM_DATA)
# --no-tick Copy only; do not ask the MCP server to extract
# --flush Copy, then graduate pending facts (compact wake-up; does not extract)
Set cwd on the source unless you intend to ingest every project group. Do not also run log-event hooks on this store.
factmem consolidate
factmem consolidate
# Options:
# --data Data directory (default: ~/.factmem or FACTMEM_DATA)
Honours the configured provider (by default claude -p). Prints JSON — facts graduated, entities extracted, duplicates and contradictions resolved.
factmem signal [tick|flush]
factmem signal tick # extract if the event threshold is due
factmem signal flush # graduate pending facts — PreCompact / shutdown
# Options:
# --data Data directory (default: ~/.factmem or FACTMEM_DATA)
pull --flush is what a PreCompact hook calls so new lines are copied and pending facts survive a context collapse. If no server is listening, flush falls back to an in-process heuristic graduate — deliberately, because compaction is time-critical. A tick that finds no server simply exits.
factmem search <query>
factmem search "coffee"
factmem search "coffee" --domain preferences
factmem search "coffee" --json
# Options:
# --domain Prioritise a domain. Biases ranking; does not filter
# --limit Maximum results (default: 20)
# --json Emit the raw search payload
# --data Data directory (default: ~/.factmem or FACTMEM_DATA)
--domain biases ranking rather than filtering. A hard filter would hide a fact filed under a near-synonym.
factmem stats
factmem stats
factmem stats --json
Facts are immutable — superseded facts are kept — so the current count and the total legitimately differ once anything has been superseded. --json includes the answering binary's package version. Intelligence spend is calls, tokens, and elapsed time for extract / classify / entities / reconcile / supersede / summarise, with provider and model per stage. Embeddings are not that number.
factmem inspect
Sample D, I, K, entities, and the graph. Writes a local HTML file under the data directory (not the cwd). Prints the path. Does not open a browser. The file is a memory export — treat it like stats --json. The same page also shows intelligence spend (Graph / Spend).
factmem inspect
factmem inspect --graph
factmem inspect --layer k
factmem inspect --json
factmem inspect --entity Helios --limit 20 --output ~/inspect.html
--layer health|d|i|k|entities|graph|all prints terminal tables (newest-first, capped). --graph (the default when no --layer / --json) writes inspect.html. --limit is 10 for tables and 50 for the canvas. --all draws every node — a hairball, explicit. Search and type filter in the page can still reach a node that was outside the cap.
Advanced
Another store
You do not need two installs. The default is one directory and one MCP server named factmem. A second store is a second directory — not a filter on which client wrote the row. Work and personal is one reason to split, not a required setup.
A non-default data directory prints a distinct MCP server name so two stores can share one mcp.json. Init against each extra directory prints that snippet. Example:
{
"mcpServers": {
"factmem-personal": {
"command": "npx",
"args": ["-y", "@factmem/mcp@0.24.2"],
"env": { "FACTMEM_DATA": "C:\\Users\\alex\\.factmem-personal" }
},
"factmem-work": {
"command": "npx",
"args": ["-y", "@factmem/mcp@0.24.2"],
"env": { "FACTMEM_DATA": "C:\\Users\\alex\\.factmem-work" }
}
}
}
Point each store's sources.cwd (or hook --data) at that store only. Two directories do not isolate anything if both pull the same home.
Postgres (optional)
SQLite is the default and needs no extra software. To use Postgres instead, set storage.provider to "postgres" in that store's config.json, or FACTMEM_STORAGE=postgres on the MCP entry, and set FACTMEM_POSTGRES_URL to a postgres:// (or postgresql://) URL. The password belongs in the environment, not in config.json. If the URL is missing or the server cannot be reached, FactMem stops; it does not create a SQLite file.
The data directory is still the memory: config.json and the scheduler socket live there. Tables live at the URL. Two memories need two directories and two databases.
Init does not ask which engine to use. factmem init --yes still writes sqlite.
Example — placeholders only; do not put a real password in a committed file:
{
"mcpServers": {
"factmem": {
"command": "npx",
"args": ["-y", "@factmem/mcp@0.24.2"],
"env": {
"FACTMEM_DATA": "C:\\Users\\alex\\.factmem-work",
"FACTMEM_STORAGE": "postgres",
"FACTMEM_POSTGRES_URL": "postgres://USER:PASSWORD@localhost:5432/factmem"
}
}
}
}
Pull versus log-event
Choose one mechanism per store.
Recommended — pull. Name a claude-code or cursor source (set cwd) and run factmem pull from the CLI first. The MCP server also copies at session start and when it handles a call. Grok and Codex are later adapters. Unknown kind values are rejected.
{
"sources": [
{
"kind": "claude-code",
"home": "~/.claude",
"cwd": "C:\\dev\\app"
}
]
}
home is the client config dir (~/.claude or ~/.cursor — path examples, not extra discovery). Cursor is "kind": "cursor" and home/projects/*/agent-transcripts/**/*.jsonl only — not Composer SQLite. Cursor encodes C:\\dev\\app as c-dev-app (Claude Code uses C--dev-app). A first pull of more than 50 events needs factmem consolidate.
Alternative — log-event, no sources. Leave sources empty. Pipe a client hook payload into factmem log-event if you have one. MCP log_event / capture_fact keep working.
Do not install log-event hooks on this store — both write the same rows. FactMem does not detect or rewrite existing hook configs.
Hooks (after the first CLI pull)
mcp.json env is not visible to hooks. Pass the same --data (or set FACTMEM_DATA in the environment the client itself inherits). The command must invoke the CLI (factmem or the openmemory shim), never the server binary. npx -y @factmem/mcp with no -p / factmem starts the MCP server and hangs a hook. Pin the package version, quote it if the hook runs PowerShell, and put -- before factmem so a globally installed older binary on PATH cannot win. Existing hooks that call openmemory keep working.
PreCompact hook JSON
{
"hooks": {
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "npx -y -p @factmem/mcp@0.24.2 -- factmem pull --flush --data /absolute/path/to/the-same-store"
}
]
}
]
}
}
PreCompact pull --flush copies new JSONL lines, then graduates pending facts already in the database. It does not extract. We do not install a turn-end Stop hook. On Windows the --data path is the same absolute directory you put in FACTMEM_DATA (for example C:\\Users\\alex\\AppData\\Local\\Temp\\factmem-try).
Frequent incremental pull interleaves conversations on the global sequence: a long chat kept open is sliced between other chats. Extract progress is per conversation, so a timeout in one chat does not discard another. Shrinking extraction.batch_size means more extract calls (more chances of a timeout), not a store-wide hold-all. factmem stats reports unextracted events against that extract watermark.
If the MCP server does not start, or lists no tools, check the package version the client actually spawned. A global factmem or openmemory on PATH can be years behind the pin in this README. Diagnose with factmem stats --data <dir> (the CLI prints whether the scheduler is listening) and by inspecting serverInfo.version from initialize plus tools/list over stdio. 0.2.x answers initialize then throws on tools/list.
Embeddings, model, timeout, bitemporal
Set embedding.provider in config.json to "ollama" (local, no API key) or "voyage" (hosted), run factmem consolidate, and search "food" starts returning the allergy. Facts are embedded when they are consolidated. Voyage applies a 3 requests/minute rate limit until a payment method is on the account.
Meaning-search is an exact scan of stored vectors when the set is small. When that set is large (default 32 MiB of the current model), an HNSW index of those vectors is used instead: in-process on SQLite, or a Postgres vector sidecar when the extension is enabled. Small stores stay exact. A missing engine keeps exact search and prints a warning; FactMem does not install a native addon. embedding.ann is null (auto), false (never), or true (force when the engine allows). This does not turn embeddings on.
intelligence.cli.model and intelligence.cli.timeout_ms are extra knobs. First-run More settings (Y) can write them; later, factmem settings. Init does not ask intelligence.provider; FACTMEM_PROVIDER=heuristic is the kill-switch. The heuristic fallback does not extract facts from transcripts.
Unnamed user-channel speech is attributed to the store's owner; a display name still does not create a person. Extra backing (assent, a tool observation, a different speaker restating) is recorded, not scored, unless the store sets interlocutor ranking weights in config.json. The engine ships none. Weight keys match the speaker string as stored, so two people with the same name share a key.
Set temporal.mode to bitemporal to record when the system retracted a belief, so search can answer what the store believed at an instant.
Intelligence spend
factmem stats and get_stats report billed consolidation calls: tokens, elapsed time, and the provider plus model on each stage (extract, classify, entities, reconcile, supersede, summarise). A run that did not report tokens omits those fields rather than showing zero. Embeddings are a different API and are not this number.
Optional intelligence.token_budget caps billed extract per provider on rolling windows. Unset is unlimited. Over the cap, consolidate skips extract, holds the watermark, and does not fall back to the heuristic. Stats and inspect Spend show used and remaining on each cap, and when oldest usage in that window ages out (resets).
"intelligence": {
"token_budget": {
"cli": { "week": "10M" }
}
}
hour, day, week, and month are rolling. Omit a scale to leave it unlimited. Remaining room is on factmem stats, get_stats, and inspect Spend. Set the cap in this store's config.json — there is no budget command.
Optional local intelligence is a different switch from embeddings. Add intelligence.http on an OpenAI-compatible host. The protocol is POST /v1/chat/completions; only the port changes:
| Host | Typical URL |
|---|---|
| Ollama | http://localhost:11434/v1 (the default if you omit the URL) |
| LM Studio | http://localhost:1234/v1 |
| vLLM | http://localhost:8000/v1 |
| llama.cpp | http://localhost:8080/v1 |
The model string is whatever that host lists. GET {base_url}/models prints the names. nomic-embed-text is embed-only and will not extract. If the host is up and serves exactly one chat model, FactMem uses it for this run and tells you to pin intelligence.http.model. If several chat models are listed, set that field; extract will not guess.
Extract and summarise then use that host; reconcile and supersede stay on the CLI unless you list intelligence.stages. Each stage can set on-fail to cli, http, or none (see the JSON below). HTTP extract defaults to retrying on the CLI (counts against the CLI token budget). Contradiction defaults to none — no provider switch. none holds the extract watermark — it does not fall through to the heuristic. First-run More settings (Y, after the recommended path) can set the host, model, and extract on-fail. Later, factmem settings merges those knobs into an existing file without resetting it. factmem inspect Spend shows the same knobs and copies JSON; it does not save config.json.
The live script npm run test:http-intelligence has passed on qwen2.5vl:7b.
"intelligence": {
"http": {
"base_url": "http://localhost:11434/v1",
"model": "qwen2.5vl:7b"
},
"stages": {
"extract": { "provider": "http", "on_fail": "cli" },
"summarise": { "provider": "http", "on_fail": "cli" },
"reconcile": { "provider": "cli", "on_fail": "none" },
"supersede": { "provider": "cli", "on_fail": "none" }
}
}
CLI demo (no transcript source)
Throwaway store, not the capture path for a real Claude Code or Cursor home. These three lines are typed in.
export FACTMEM_DATA=/tmp/factmem-demo
om() { npx -y -p "@factmem/mcp@0.24.2" -- factmem "$@"; }
om init --yes
om log-event --role user --content "I prefer dark mode in every editor, and I never want telemetry enabled."
om log-event --role user --content "I am allergic to shellfish, so avoid seafood restaurants when booking anything."
om log-event --role user --content "My colleague Robin at Acme is leading the Atlas migration project this quarter."
om consolidate
om search "Atlas"
om stats
$env:FACTMEM_DATA = Join-Path $env:TEMP "factmem-demo"
function om { npx -y -p "@factmem/mcp@0.24.2" -- factmem @args }
om init --yes
om log-event --role user --content "I prefer dark mode in every editor, and I never want telemetry enabled."
om log-event --role user --content "I am allergic to shellfish, so avoid seafood restaurants when booking anything."
om log-event --role user --content "My colleague Robin at Acme is leading the Atlas migration project this quarter."
om consolidate
om search "Atlas"
om stats
allergies is not a domain FactMem ships. The engine has no built-in vocabulary — it read the conversation and decided that fact needed a home. A domain biases ranking rather than filtering. Clean up: rm -rf /tmp/factmem-demo (Git Bash / macOS / Linux) or Remove-Item -Recurse -Force $env:TEMP\factmem-demo (PowerShell).
Integration
FactMem's tool descriptions tell assistants when to search and when a correction is worth staging. They are not how Claude Code conversations enter the store — that is pull from a named source.
Without configuration
Claude Code or Cursor: name a sources entry (set cwd) and pull from the CLI first. MCP session start also pulls. capture_fact is there if the assistant needs to correct or add something pull-plus-extraction will not produce.
Clients with no pull adapter still rely on log_event / capture_fact until their adapter exists.
Hook points
| Hook point | When | What to call | Why |
|---|---|---|---|
| Session start | Conversation begins | memory://profile (automatic), search_knowledge | The assistant knows who you are from message one |
| Correction | A durable fact is missing from the store | capture_fact | Optional; Claude Code conversations are already in session_events via pull |
| Pre-response search | Before generating a reply | search_knowledge, get_context | Responses informed by stored knowledge |
| Pre-compaction | Before context window compression | factmem pull --flush | Copies new lines, then graduates pending facts |
| Natural breakpoints | Topic change, task completion | consolidate (optional) | Keeps the knowledge graph current |
On pre-compaction: factmem pull --flush copies new transcript lines, then graduates what extract already wrote. It does not extract. It is not a log-event hook.
Claude Code
Create .claude/rules/factmem.md in your project (or ~/.claude/rules/factmem.md globally):
# FactMem
- Conversations are pulled from the named Claude Code source (first backfill: `factmem pull` on the CLI)
- Do not install log-event hooks on this store
- Identity context loads automatically from the `memory://profile` resource — no tool call needed
- Before answering questions this store might already know, call `search_knowledge`
- Call `capture_fact` only to correct or add something that is not in the transcript
- When the conversation is getting long, call `consolidate` (or rely on PreCompact `factmem pull --flush`)
- At natural breakpoints (topic change, task completion), call `consolidate` to keep the knowledge graph current
To allow FactMem tools without per-call approval prompts, add to the permissions.allow array in .claude/settings.json:
{
"permissions": {
"allow": [
"mcp__factmem__*"
]
}
}
Cursor / Windsurf
Add to .cursorrules (Cursor) or .windsurfrules (Windsurf) in your project root:
When the factmem MCP server is available:
- Before answering questions this store might already know, call search_knowledge
- To find out everything known about a particular person, project, or thing, call get_entity
- Call capture_fact only to correct or add something pull or extraction missed
- When context is getting long, call consolidate to process pending facts before they are lost
Cursor and Windsurf consume tools but not resources, so memory://profile will not load on its own there. Cursor conversations themselves are pulled with kind: "cursor" (JSONL under ~/.cursor/projects/, not the SQLite composer store).
Claude Desktop / other MCP clients
No pull adapter yet. Tool descriptions handle search and optional capture_fact; conversations are not tailed until a later adapter exists.
Reclaiming space
FactMem logs raw conversation and tool output to session_events. On a store wired into an agentic client this becomes almost all of the database. A store measured in daily use held 47,000 events and 493 MB against 21 graduated facts.
factmem stats reports the raw layer alongside the knowledge, including how much is reclaimable. To reclaim it:
factmem prune # report only — nothing is deleted
factmem prune --apply --vacuum # delete, then rebuild the file
Set retention.disk_budget in config.json to a size such as "2GB" to cap memory.db. Unset is unlimited; init does not write a cap. When a cap is set and the file is full, unreachable raw events are pruned automatically so new logs can reuse that space; if nothing unused remains, more raw events are refused. Facts are never deleted to meet the number. Compacting (--vacuum) is still a human step — it copies the whole file so the operating system sees the smaller size.
If most of that volume is tool output you judge to be noise, extraction.event_types and extraction.roles restrict what is examined, and extraction.min_content_length skips trivial events. Measure before you do. Volume and value are not the same axis.
The rule is reachability, not age. An event is removed only when all three hold:
- Extraction has already read it. Anything ahead of the consolidation watermark is still input.
- No fact's provenance cites it.
- It has fallen outside its own session's most recent
extraction.working_memory_sizeevents — a spare so consolidation can still glance at recent raw notes. That window is evidence of the current topic, not a pronoun dictionary.
No fact, entity, embedding or search result is affected. Deleting rows does not shrink the file on its own — that is --vacuum. Without a cap, nothing prunes automatically.
Development
git clone https://github.com/gordonkjlee/factmem
cd factmem
npm install
npm run build
npm test
npm test always runs hermetic pipelines (fixture JSONL → pull → extract →
search) with a recording extractor, and skips live evals that need a real
model:
- Semantic recall needs Ollama with
nomic-embed-text. Start it, thennpm run test:semantic. - The live first-fact eval needs the
claudeCLI. Runnpm run test:first-fact. - The live coding-store eval (warehouse-shaped Cursor transcripts) also
needs the
claudeCLI. Runnpm run test:coding-store. - Local HTTP extract needs a chat model on an OpenAI-compatible host and
FACTMEM_HTTP_MODEL(verified onqwen2.5vl:7b). Runnpm run test:http-intelligence.
Each of those scripts fails rather than skips when its dependency is missing, so a green run means the claim was actually verified rather than quietly stepped over.
Contribute
Issues and pull requests are welcome. Open an issue first if the change is more than a typo.
- Questions: GitHub Discussions
- How to build and test: CONTRIBUTING.md
License
MIT