# A memory's full version chain Source: https://docs.memloom.dev/api-reference/a-memorys-full-version-chain /openapi.yaml get /memory/{id}/history Every version of the belief, newest first, including stale ones. History is the one place a stale version is still visible; recall never returns them. Any version's id in the chain works as `id`. # A session's attached files Source: https://docs.memloom.dev/api-reference/a-sessions-attached-files /openapi.yaml get /assistant/sessions/{id}/attachments # A session's messages Source: https://docs.memloom.dev/api-reference/a-sessions-messages /openapi.yaml get /assistant/sessions/{id}/messages Oldest first. Assistant messages carry the sources their answer cited. # Add a user-tier vocabulary entry Source: https://docs.memloom.dev/api-reference/add-a-user-tier-vocabulary-entry /openapi.yaml post /memory/schema Add an entity type or predicate to the extraction vocabulary. The name is normalized to snake_case; the description becomes part of the extraction prompt, so write it like a rule ("a named drug or supplement"). Takes effect on the next index run. # Approve a proposal Source: https://docs.memloom.dev/api-reference/approve-a-proposal /openapi.yaml post /memory/schema/{id}/approve Promote a proposed name to the user tier and link its saved occurrences into the graph immediately (no re-index needed). The extractor also starts using the name on the next index run. # Attach a file to a chat Source: https://docs.memloom.dev/api-reference/attach-a-file-to-a-chat /openapi.yaml post /assistant/attachments Upload file bytes into one chat's scope. The file is chunked and embedded like a document, but only that session's recall searches it: it stays out of the documents list, the graph, and entity extraction, and it is deleted with the chat. Omit `sessionId` to create the session on the spot (titled 'New chat' until the first message names it). Body limit ~48MB (~36MB of file). Remove an attachment with DELETE /context/documents/{id}. # Auto-index state Source: https://docs.memloom.dev/api-reference/auto-index-state /openapi.yaml get /memory/auto-index Whether new memories and files are entity-indexed automatically in the background (debounced runs a few seconds after a save/ingest). `available` is false in offline mode: extraction needs an LLM, so there is nothing to toggle. # Auto-resolve pending conflicts with an LLM judge (NDJSON stream) Source: https://docs.memloom.dev/api-reference/auto-resolve-pending-conflicts-with-an-llm-judge-ndjson-stream /openapi.yaml post /memory/conflicts/resolve-auto/stream Re-judges every pending conflict with context the save-time classifier never had: when each side was recorded, and for imported memories the transcript excerpt behind it. Only a decisive verdict resolves anything, applied through the same path as the manual endpoint and just as revertable. `unsure` leaves the conflict pending. NDJSON: one `{"type":"item"}` per conflict, then `{"type":"done"}` with totals. Needs a model. # Clear the whole indexing history Source: https://docs.memloom.dev/api-reference/clear-the-whole-indexing-history /openapi.yaml delete /memory/index/runs # Confirm or dismiss a possible contradiction Source: https://docs.memloom.dev/api-reference/confirm-or-dismiss-a-possible-contradiction /openapi.yaml post /memory/reconcile/possible/{id}/answer `approved` promotes it into a real conflict and returns the conflict id, so it is then resolved and reverted like any other. `rejected` records the pair so it is never raised again. # Continuous capture status Source: https://docs.memloom.dev/api-reference/continuous-capture-status /openapi.yaml get /import/claude-code/status What `memloom status` renders for `connect claude-code`: the configured capture scope, when the hook last fired and whether it failed, today's unattended distillation spend against the daily cap, and ledger totals across every import so far (sessions and memories, regardless of trigger). # Delete a disabled user-tier entry Source: https://docs.memloom.dev/api-reference/delete-a-disabled-user-tier-entry /openapi.yaml delete /memory/schema/{id} Permanently remove a vocabulary entry. Only **disabled user-tier** entries qualify: built-in entries are re-seeded by name on the next run (a delete would bring them back active), so disabling is the only way to turn them off, and proposals are approved or dismissed instead. Entities already extracted under the deleted type stay in the graph. # Delete a memory's whole belief Source: https://docs.memloom.dev/api-reference/delete-a-memorys-whole-belief /openapi.yaml delete /memory/{id} Deletes every version in the lineage, its edges, and any pending conflict that names it. Unlike a resolution's `stale`, this is permanent; there is no revert. # Delete a session Source: https://docs.memloom.dev/api-reference/delete-a-session /openapi.yaml delete /assistant/sessions/{id} Removes the session, its messages, and its attached files. # Delete all chat sessions Source: https://docs.memloom.dev/api-reference/delete-all-chat-sessions /openapi.yaml delete /assistant/sessions Removes every session, its messages, and its attachments. # Delete an entity Source: https://docs.memloom.dev/api-reference/delete-an-entity /openapi.yaml delete /memory/entities/{id} Remove the entity and every edge touching it (mentions pointing at it, typed relationships from or to it). The memories and documents that mentioned it are untouched. Refused with 400 while an unreverted fold points at this entity: the alias row carries the absorbed entity's id and vector, so deleting would make that fold permanent. Revert the fold first. # Delete one run session (its events cascade) Source: https://docs.memloom.dev/api-reference/delete-one-run-session-its-events-cascade /openapi.yaml delete /memory/index/runs/{id} # Dismiss a proposal Source: https://docs.memloom.dev/api-reference/dismiss-a-proposal /openapi.yaml post /memory/schema/{id}/dismiss Reject a proposed name. The prompt blocklists it, so the extractor never proposes it again. # Distill agent sessions into memories Source: https://docs.memloom.dev/api-reference/distill-agent-sessions-into-memories /openapi.yaml post /import/sessions/stream Distills local Claude Code transcripts into typed memories. Per session: check the ledger, redact secret-shaped strings, chunk, distill, embed, and save through the belief pipeline. The path is fixed to `~/.claude/projects`; a client cannot pass an arbitrary one. NDJSON: one `{"type":"item"}` per session, then `{"type":"done"}` with totals and the cost line. A dry run makes zero model calls and writes nothing. # Edit a memory into a new version Source: https://docs.memloom.dev/api-reference/edit-a-memory-into-a-new-version /openapi.yaml post /memory/{id}/update An explicit edit: unlike a `save` that happens to resemble an existing memory, this always creates a new version, no dedup classifier involved. The target must be an active belief; the edited version goes stale and stays visible in `history`. # Enable or disable a vocabulary entry Source: https://docs.memloom.dev/api-reference/enable-or-disable-a-vocabulary-entry /openapi.yaml patch /memory/schema/{id} Disabled entries leave the extraction prompt; entities already extracted under them stay in the graph. Works on system and user tiers (proposals have their own approve/dismiss lifecycle). # Find and fold duplicate entity spellings Source: https://docs.memloom.dev/api-reference/find-and-fold-duplicate-entity-spellings /openapi.yaml post /memory/entities/resolve One resolution pass over the entity table. Spellings that are identical apart from case and punctuation are folded without asking; uncertain pairs are written to the conflicts queue for review. Idempotent and safe to re-run. The queue is capped so entity folds cannot bury memory conflicts. Questions that did not fit are reported as `deferred` rather than dropped silently, and a later pass picks them up as answers come in. # Forget a watched folder Source: https://docs.memloom.dev/api-reference/forget-a-watched-folder /openapi.yaml delete /context/roots/{id} Removes the folder from the watch list and KEEPS every document it produced. "Stop following this folder" is never "delete what was read in it"; deleting those is DELETE /context/documents/{id}, one at a time and deliberately. # Get a document's chunks and their entity edges Source: https://docs.memloom.dev/api-reference/get-a-documents-chunks-and-their-entity-edges /openapi.yaml get /context/documents/{id}/chunks One document at chunk granularity: chunks in order, plus their chunk -> entity mention edges. The graph endpoint rolls these up to one weighted document -> entity edge; this is the drill-down the viewer fetches when a document node is expanded. # Get the memory graph Source: https://docs.memloom.dev/api-reference/get-the-memory-graph /openapi.yaml get /memory/graph Returns active memories, extracted entities, and context documents as nodes, with the edges between them. Chunk-level mention edges are rolled up to one weighted document -> entity edge, so documents stay single nodes. # Get the reconcile settings Source: https://docs.memloom.dev/api-reference/get-the-reconcile-settings /openapi.yaml get /memory/reconcile/settings Which passes a run does, and whether the daemon catches up on startup. Stored in the store rather than in `config.env`, so a change takes effect without a restart. # Health check Source: https://docs.memloom.dev/api-reference/health-check /openapi.yaml get /health Returns ok when the daemon is up. Used by clients to detect a running daemon. # Import memories agents already saved on disk Source: https://docs.memloom.dev/api-reference/import-memories-agents-already-saved-on-disk /openapi.yaml post /import/agent-memory/stream What `memloom import agent-memory` calls. Discovers the agents' own well-known memory folders (Claude Code per-project memory directories, GitHub Copilot's memory-tool notes; the client never supplies a filesystem path), parses each file into memories, redacts secret-shaped strings, and saves through the belief pipeline. No LLM extraction: the files are already distilled. A content-hash ledger skips unchanged memories, so re-runs are free. The response streams NDJSON: one `{"type":"item"}` event per folder, then `{"type":"done"}` with the totals. # Index memories and context chunks (extract entities) Source: https://docs.memloom.dev/api-reference/index-memories-and-context-chunks-extract-entities /openapi.yaml post /memory/index Extract entities from memories AND context chunks that have not been indexed yet and link them into the graph with mention edges. Runs the LLM extractor (one call per row, so large documents take proportionally longer); a no-op in offline mode. # Index sessions (persistent run history) Source: https://docs.memloom.dev/api-reference/index-sessions-persistent-run-history /openapi.yaml get /memory/index/runs Every index/reindex pass is recorded as a run: trigger (index|rebuild), status (running|success|warning|error|interrupted), batch size, per-kind indexed counts, failures, and entity/relation totals. Newest first. The viewer's Console polls this while a run is live; history survives restarts and includes CLI runs. # Index with real-time progress (NDJSON stream) Source: https://docs.memloom.dev/api-reference/index-with-real-time-progress-ndjson-stream /openapi.yaml post /memory/index/stream Same work as /memory/index, but the response streams newline-delimited JSON as it runs: one `{"type":"item"}` event per processed memory/chunk (with its label, 1-based position, total, and extracted entity names), then a final `{"type":"done"}` event with the run totals. Failures mid-run arrive in-band as `{"type":"error"}`. # Ingest a file as context Source: https://docs.memloom.dev/api-reference/ingest-a-file-as-context /openapi.yaml post /context/add Extract, chunk, embed, and store a .md/.txt/.pdf file from the daemon's machine. A document mirrors its file: re-adding an unchanged file is a no-op (`unchanged`), a changed file replaces its chunks (`updated`). Markdown chunks carry their heading breadcrumb; PDF chunks keep their page number. Chunks surface in normal `/memory/query` results with a `source`. # Let a model settle the queued entity pairs Source: https://docs.memloom.dev/api-reference/let-a-model-settle-the-queued-entity-pairs /openapi.yaml post /memory/entities/resolve-auto Asks a model about every pair the lexical rules could not settle, folding the ones it is confident about and leaving the rest queued. One call per pair, so it is never a side effect of anything else. This is the same work the `llm_entities` reconcile pass does. Refused with 403 when `RECONCILE_ENABLED=0`, because it spends money. # Let a model settle the queued entity pairs (NDJSON stream) Source: https://docs.memloom.dev/api-reference/let-a-model-settle-the-queued-entity-pairs-ndjson-stream /openapi.yaml post /memory/entities/resolve-auto/stream The same work, reported as it goes. One `{"type":"item"}` per pair judged, a `{"type":"ping"}` every 15 seconds so the pipe never times out, and a final `{"type":"done"}` carrying the totals. A mid-stream failure arrives as `{"type":"error"}`, since the response has already started. # List all active memories Source: https://docs.memloom.dev/api-reference/list-all-active-memories /openapi.yaml get /memory/list Every active memory, newest first. Use this to browse the store; use /memory/query for relevance-ranked recall. Stale (superseded) memories are excluded. # List chat sessions Source: https://docs.memloom.dev/api-reference/list-chat-sessions /openapi.yaml get /assistant/sessions Starred first, then most recently active. # List context documents Source: https://docs.memloom.dev/api-reference/list-context-documents /openapi.yaml get /context/documents # List entities with usage counts Source: https://docs.memloom.dev/api-reference/list-entities-with-usage-counts /openapi.yaml get /memory/entities Every extracted entity with its usage: active mention edges, distinct memories, and distinct documents that mention it. Most-mentioned first. Use this list to manage entities; /memory/graph shows the same data as a graph. # List entity folds Source: https://docs.memloom.dev/api-reference/list-entity-folds /openapi.yaml get /memory/entities/merges Every fold, newest first, including reverted ones (`revertedAt` is set). `decidedBy` says what applied it: `auto` for a deterministic spelling match, `human` for a fold you applied, `llm` for a future arbitration tier. # List entity pairs settled as different things Source: https://docs.memloom.dev/api-reference/list-entity-pairs-settled-as-different-things /openapi.yaml get /memory/entities/settled Pairs a human or a model decided are not the same thing. Recorded so the resolver never raises them again. Reverting one puts the question back in the queue. # List Notion pages the integration can see Source: https://docs.memloom.dev/api-reference/list-notion-pages-the-integration-can-see /openapi.yaml get /notion/pages What the connect picker renders. Lists every page, database, and data source shared with the configured Notion integration, with parent references so clients can show the workspace as a tree (database row-pages collapse under their database). Requires NOTION_TOKEN in the daemon's environment; the token never crosses this API. # List one run's findings Source: https://docs.memloom.dev/api-reference/list-one-runs-findings /openapi.yaml get /memory/reconcile/runs/{id}/actions Everything the run recorded, including findings held back by a per-run cap (`surfaced: false`). An unknown id reads as an empty run rather than another store's ledger. # List pending conflicts Source: https://docs.memloom.dev/api-reference/list-pending-conflicts /openapi.yaml get /memory/conflicts Contradictions the belief pipeline flagged that you have not resolved yet. Both sides stay active until you decide. # List possible contradictions Source: https://docs.memloom.dev/api-reference/list-possible-contradictions /openapi.yaml get /memory/reconcile/possible What the contradiction re-check found and nobody has confirmed, newest first. These are deliberately **not** conflicts: measured precision for that pass is roughly 40 percent, so they wait here where dismissing one costs a click. Each one carries the verbatim clashing span from both sides, verified against the two memories before the row was written, so it can be judged without opening either memory. A finding whose two memories are no longer both active drops out of this list. # List reconcile runs Source: https://docs.memloom.dev/api-reference/list-reconcile-runs /openapi.yaml get /memory/reconcile/runs Every run for this store, newest first, with what each one scanned, changed, and asked. This is the Console tab's history. A run with `status` `running` is still going and its counters move. # List resolved conflicts Source: https://docs.memloom.dev/api-reference/list-resolved-conflicts /openapi.yaml get /memory/conflicts/resolved The revertable history behind the pending queue, newest resolution first. Each entry carries the action that resolved it and can be undone via the revert endpoint. # List uncertain entity folds awaiting review Source: https://docs.memloom.dev/api-reference/list-uncertain-entity-folds-awaiting-review /openapi.yaml get /memory/entities/conflicts Pairs the resolver was not confident enough to fold. Resolve them through POST /memory/conflicts/{id}/resolve like a memory conflict: `keep_existing` folds the queued spelling into the candidate, `keep_new` folds the other way, `keep_both` settles them as different things. `merge` does not apply to entities. These never appear in GET /memory/conflicts, which is scoped to memory contradictions. # List watched folders Source: https://docs.memloom.dev/api-reference/list-watched-folders /openapi.yaml get /context/roots The folders someone linked, as opposed to the documents inside them. Adding a folder creates one document per file and would otherwise forget the folder itself, which is the only record that files arriving LATER were asked for too. `enabled` is false when the daemon has watching switched off (`MEMLOOM_SYNC=off`). "Nothing is watched" and "watching is off" are different answers, and a client that cannot tell them apart will report the wrong one. # Merge an entity into another Source: https://docs.memloom.dev/api-reference/merge-an-entity-into-another /openapi.yaml post /memory/entities/{id}/merge Fold this entity into a surviving one. Every mention and relationship repoints to the target, duplicates and self-loops are dropped, and the absorbed spelling is kept as an alias so later mentions resolve to the survivor, which keeps its own name and type. Recorded and revertable through /memory/entities/merges. No lexical rule is applied, so this is how you declare two entities the same when nothing about their spelling says so, like a nickname and a full name. # Notify the daemon a Claude Code session ended Source: https://docs.memloom.dev/api-reference/notify-the-daemon-a-claude-code-session-ended /openapi.yaml post /import/claude-code/notify What the Claude Code session-end hook installed by `memloom connect claude-code` calls. Distills exactly the one session at `path` in the background: the hook gets a 202 immediately and never waits on distillation, so a slow LLM call can't hang the editor. `path` must resolve inside `~/.claude/projects` and end in `.jsonl`; anything else is rejected before it reaches the filesystem. Subject to the daily unattended call budget reported at `/import/claude-code/status`. # Notion connector status Source: https://docs.memloom.dev/api-reference/notion-connector-status /openapi.yaml get /notion/status Whether a token is configured, the current selection, the last sync time and error, whether a sync is running, and how many notion:// documents exist. # One assistant turn (SSE) Source: https://docs.memloom.dev/api-reference/one-assistant-turn-sse /openapi.yaml post /assistant/chat Run one chat turn through the assistant's agentic loop: the model decides whether to call hybrid recall (up to three times), then streams a grounded answer with `[n]` citation markers validated against the returned sources. The response is a server-sent-event stream of JSON events: `tool_call` and `tool_result` while the model searches, `delta` per answer fragment, then one terminal `done` (sessionId, messageId, answer, sources) or `error`. Omit `sessionId` to start a new chat (titled from this message). `model` overrides the configured chat model for this turn only; it must support tool calling on OpenRouter. Requires a chat-capable LLM: offline mode answers 503 with a setup hint. # Open the OS-native file/folder picker Source: https://docs.memloom.dev/api-reference/open-the-os-native-filefolder-picker /openapi.yaml post /context/pick What the viewer's Browse buttons call first. Opens the platform's native file or folder dialog on the daemon's machine and returns the chosen absolute paths. Returns 501 on a system with no native picker (headless Linux without zenity); the viewer falls back to /context/browse in that case. # Open the source file on the daemon's machine Source: https://docs.memloom.dev/api-reference/open-the-source-file-on-the-daemons-machine /openapi.yaml post /context/documents/{id}/open Opens the document's file with the OS default application (what the viewer's "Open file" button calls). Only ingested documents can be opened: the path is read from the document row looked up by id, so a client can never supply an arbitrary path to open. # Put a folder on the watch list Source: https://docs.memloom.dev/api-reference/put-a-folder-on-the-watch-list /openapi.yaml post /context/roots Adds a folder to the watch list without ingesting anything. Idempotent by path: re-adding a folder already on the list turns watching back on rather than making a second entry. Every add route records the folder it was given, so this is only needed when nothing is being ingested. The CLI uses it because it walks a folder itself, to report per-file progress, and then adds one FILE at a time: nothing it sends would otherwise tell the daemon a folder was involved. An EMPTY folder is a normal thing to link. Pointing a recorder at a folder means linking it before the first file exists. # Rebuild the graph from scratch Source: https://docs.memloom.dev/api-reference/rebuild-the-graph-from-scratch /openapi.yaml post /memory/reindex Wipes every extracted artifact (all entities, their mention edges, and all typed entity-to-entity edges), then re-runs indexing over everything. Belief edges (replaces, distinct) are never touched. Use it to recover when an older extraction pipeline polluted the graph. # Rebuild with real-time progress (NDJSON stream) Source: https://docs.memloom.dev/api-reference/rebuild-with-real-time-progress-ndjson-stream /openapi.yaml post /memory/reindex/stream Same work as /memory/reindex, streamed as newline-delimited JSON in the same shape as /memory/index/stream (item events, then done; errors in-band). # Recall memories Source: https://docs.memloom.dev/api-reference/recall-memories /openapi.yaml post /memory/query Hybrid retrieval over active memories: vector similarity + exact keyword + entity match, fused with reciprocal-rank fusion. Results are ordered by `rrfScore`, which is rank-based and small by design (about 0.03 max). `similarity` is the plain cosine similarity score. # Remove a context document Source: https://docs.memloom.dev/api-reference/remove-a-context-document /openapi.yaml delete /context/documents/{id} Deletes the document and all of its chunks (cascade). The source file on disk is untouched. # Rename or retype an entity Source: https://docs.memloom.dev/api-reference/rename-or-retype-an-entity /openapi.yaml patch /memory/entities/{id} A rename re-embeds the entity, since the embedding is computed from the name. It is refused with 409 when another entity already owns that name key; use merge instead. A retype must name an active vocabulary type. # Rename or star a session Source: https://docs.memloom.dev/api-reference/rename-or-star-a-session /openapi.yaml patch /assistant/sessions/{id} # Resolve a conflict Source: https://docs.memloom.dev/api-reference/resolve-a-conflict /openapi.yaml post /memory/conflicts/{id}/resolve Apply one of the four human-in-the-loop resolutions. Every resolution is non-destructive and reversible via the revert endpoint: losing memories are marked `stale` and kept in the store. # Revert a resolution Source: https://docs.memloom.dev/api-reference/revert-a-resolution /openapi.yaml post /memory/conflicts/{id}/revert Undo a previous resolution. Staled memories come back to active and the conflict returns to the pending list. # Run the consolidation pass Source: https://docs.memloom.dev/api-reference/run-the-consolidation-pass /openapi.yaml post /memory/reconcile Reviews the store, repairs what SQL proves wrong, folds duplicate entity names, and asks about the rest. Every run is one undoable unit: the report names the run id, and `/memory/reconcile/{id}/revert` puts back exactly what it did. `mode` defaults to `dry_run`, which changes nothing and never calls a model, returning what the paid passes would have cost in `estimate`. **The `memloom reconcile` CLI command defaults the other way and applies.** Passes come from the saved settings unless `passes` overrides them: two free ones on, three paid ones off. With the contradiction re-check on a run takes minutes, so use the stream variant rather than holding this request open. See [Reconciliation](https://docs.memloom.dev/concepts/reconciliation). # Run the consolidation pass (NDJSON stream) Source: https://docs.memloom.dev/api-reference/run-the-consolidation-pass-ndjson-stream /openapi.yaml post /memory/reconcile/stream The same run, reported as it goes. NDJSON: one `{"type":"item"}` per belief checked, a `{"type":"ping"}` every 15 seconds so the pipe never times out, and a final `{"type":"done"}` carrying the report. A mid-stream failure arrives as `{"type":"error"}`, since the response has already started. **Disconnecting stops the run**, so nothing more is spent. Every belief already checked stays checked. The first event names the run id. # Save a memory Source: https://docs.memloom.dev/api-reference/save-a-memory /openapi.yaml post /memory/save Save a memory through the belief pipeline: exact-duplicate detection, semantic dedup, and contradiction detection. A contradiction keeps **both** memories active and returns `outcome: conflict` with a `conflictId` for you to resolve. Nothing is overwritten. In offline mode (no `OPENROUTER_API_KEY`) dedup is off and every save is `added`. # Search chat sessions Source: https://docs.memloom.dev/api-reference/search-chat-sessions /openapi.yaml get /assistant/sessions/search Hybrid chat search: keyword over titles and message text, plus embedding similarity over message content. Each hit carries the best-matching snippet. # Server-side directory listing Source: https://docs.memloom.dev/api-reference/server-side-directory-listing /openapi.yaml get /context/browse Lists one directory on the daemon's machine: subdirectories, plus files whose extension has a registered extractor. The viewer's fallback file/folder picker for systems with no native dialog; the browser never sees absolute paths from its own file inputs, only what this endpoint returns. # Set the continuous-capture allowlist Source: https://docs.memloom.dev/api-reference/set-the-continuous-capture-allowlist /openapi.yaml put /import/claude-code/scope What `memloom connect claude-code --project X` and `--all` set. `null` (the default) captures nothing: naming a scope is the consent step, since the hook fires for every Claude Code session on the machine, including client work and repos whose code should never reach an LLM provider. `{"projects":[...]}` captures only sessions whose project folder name contains one of the given substrings; `"all"` captures every project. # Set which Notion items sync Source: https://docs.memloom.dev/api-reference/set-which-notion-items-sync /openapi.yaml put /notion/scope Replaces the sync selection. Pass the items to sync, or null to disconnect (synced documents stay). What `memloom notion connect` and the viewer's connectors tab call. # Start or stop watching a folder Source: https://docs.memloom.dev/api-reference/start-or-stop-watching-a-folder /openapi.yaml post /context/roots/{id}/watch Its documents and their chunks are untouched either way. Switching it back on re-walks the folder on the next pass, so anything that changed while it was off is picked up. # Start or stop watching one document Source: https://docs.memloom.dev/api-reference/start-or-stop-watching-one-document /openapi.yaml post /context/documents/{id}/watch Switches file watching for a single document, without touching the folder it came from. Nothing already learned changes: the chunks, their embeddings and their entity edges stay exactly as they are, and switching it back on re-checks the file on the next pass. Only meaningful for a document with a file behind it (`watchable`). Uploads, chat attachments and web pages have no path to watch. # Stop a running reconcile Source: https://docs.memloom.dev/api-reference/stop-a-running-reconcile /openapi.yaml post /memory/reconcile/{id}/stop Abort the run if this daemon is the one running it, and mark the row either way. A run whose daemon died mid-sweep has no controller left, and this is the only way its row ever leaves `running`. Beliefs the run already checked stay checked, so stopping costs nothing beyond the calls already paid for. # Stop the daemon Source: https://docs.memloom.dev/api-reference/stop-the-daemon /openapi.yaml post /admin/shutdown Gracefully stop the daemon (what `memloom stop` calls): acknowledges, then closes the Postgres wire server, the HTTP server, the store, and releases the data-dir lock. # Sync the selected Notion items now Source: https://docs.memloom.dev/api-reference/sync-the-selected-notion-items-now /openapi.yaml post /notion/sync/stream What `memloom notion sync` calls (the daemon also runs it on a poll timer). For each selected item: asks Notion whether it changed, refetches only the sections whose last_edited_time moved (cached block tree), renders to markdown, and re-ingests chunk-stably so unchanged chunks keep their rows and embeddings. The response streams NDJSON: one {"type":"item"} event per page plus fetch-progress events, then {"type":"done"} with the totals. A dry run fetches no page content and writes nothing. # The full text behind a truncated recall result Source: https://docs.memloom.dev/api-reference/the-full-text-behind-a-truncated-recall-result /openapi.yaml get /memory/passage/{id} A recall hit's content can be truncated for display; this returns the complete text of one memory or context chunk by id. What the MCP `read_passage` tool and the assistant's citation links call to fetch the rest. # The graph schema registry with live counts Source: https://docs.memloom.dev/api-reference/the-graph-schema-registry-with-live-counts /openapi.yaml get /memory/schema The vocabularies the indexer extracts against: entity types and typed-relationship predicates as registry rows (tier system/user, status active/disabled), edge relations from the engine, and the LLM proposal review queue (names the extractor wanted, with occurrence counts). # The model catalog for the picker Source: https://docs.memloom.dev/api-reference/the-model-catalog-for-the-picker /openapi.yaml get /assistant/models Tool-capable models from OpenRouter's public catalog (the assistant requires native tool calling), shaped for display: USD per million input/output tokens, context window, provider (the model id's prefix). Cached daemon-side for an hour; a stale copy keeps serving if OpenRouter is unreachable. `defaultModel` is the daemon's configured chat model, or null in offline mode. # The per-item log lines of one run Source: https://docs.memloom.dev/api-reference/the-per-item-log-lines-of-one-run /openapi.yaml get /memory/index/runs/{id}/events # Turn auto-index on or off Source: https://docs.memloom.dev/api-reference/turn-auto-index-on-or-off /openapi.yaml patch /memory/auto-index The Console's toggle. Persisted in the store, so the choice survives daemon restarts; the MEMLOOM_AUTO_INDEX config value is only the default before the toggle is first used. Refused with 409 in offline mode. # Undo an entity fold Source: https://docs.memloom.dev/api-reference/undo-an-entity-fold /openapi.yaml post /memory/entities/merges/{id}/revert Restores the absorbed entity with its original id, type, vector and creation time, repoints exactly the edges the fold moved, reactivates exactly the edges it deactivated, and removes the alias. The original id matters because deactivated edges still point at it. # Undo one reconcile run Source: https://docs.memloom.dev/api-reference/undo-one-reconcile-run /openapi.yaml post /memory/reconcile/{id}/revert Put back exactly what one run did, and only that run. A retired memory is reactivated only while it is still stale and its `stale_since` is the one this run wrote, so a newer decision is never overridden. A fold goes back through the same revert entity resolution uses. A conflict the run raised and nobody answered is removed from the queue; one that was answered is left alone and counted in `skipped`. Reverting twice is a no-op. # Update the reconcile settings Source: https://docs.memloom.dev/api-reference/update-the-reconcile-settings /openapi.yaml post /memory/reconcile/settings Patch any subset. This is what the viewer's Settings tab writes. Turning on `llm_entities`, `llm_conflicts`, or `llm_recheck` is what allows a run started by hand to spend money; the daemon's own idle and startup runs stay on the free passes whatever is saved here. # Upload file bytes as a document Source: https://docs.memloom.dev/api-reference/upload-file-bytes-as-a-document /openapi.yaml post /context/upload Ingest a file whose bytes come from the client. The viewer's Upload button uses this, because a browser file dialog yields file bytes without a usable path. The document is listed, graphed, and indexed like any other. Its provenance is `upload://`, so re-uploading the same name replaces it and identical bytes are a no-op. Unlike path-linked documents there is no disk file to open or change-track; prefer /context/add when the daemon can read the file itself. Body limit ~48MB (base64 inflates by a third, so roughly a 36MB file). # Walk the graph from one entity Source: https://docs.memloom.dev/api-reference/walk-the-graph-from-one-entity /openapi.yaml get /memory/entities/related Everything connected to one entity. The target may be an id, a name, or a folded-away spelling, which resolves to its canonical with `matchedAlias` naming what was asked for. Matched exactly, not by meaning: this is a graph lookup, not a search. Two kinds of connection come back separately and are never blended. **Stated** links are typed edges extraction asserted, with direction. **Co-mention** is entities named by the same sources, which is weaker and never outranks a stated link. # Conflicts Source: https://docs.memloom.dev/cli/conflicts List pending contradictions, or let a model clear the queue ## conflicts ```bash theme={null} memloom conflicts ``` Lists pending contradictions: the new memory and the existing ones it clashes with. Both stay active until settled. Resolving by hand lives where a human is: the viewer's Conflicts tab, or an agent relaying your choice through [`resolve_conflict`](/mcp/tools). The four actions are keep new, keep existing, keep both, and merge. Every resolution is reversible. *** ## conflicts auto ```bash theme={null} memloom conflicts auto ``` Re-judges every pending conflict, one call each. The classifier that filed them saw two isolated pieces of text; this adds what it could not see: when each memory was recorded, and for imported ones the transcript excerpt it came from. ``` [1/3] keep new: the deploy target is railway (the newer session shows the change) [2/3] left for you: staging runs on Postgres (times are too close) [3/3] keep both: prefers pnpm here, npm in the legacy repo (different scopes) resolved 2 of 3; 1 left for you ``` Only a decisive verdict resolves anything. The model is told to answer **unsure** rather than guess, and an unsure conflict stays exactly as it was. Resolutions land in the same revertable history as manual ones, so this is safe to re-run over a growing queue. Needs a model, so it is unavailable offline. How detection works and what each resolution does to the version chain is in [Conflicts](/concepts/conflicts). # Context Source: https://docs.memloom.dev/cli/context context add, list and remove: ingesting files as searchable context ## context add ```bash theme={null} memloom context add ``` Ingest files or folders, which recurse. Chunks embed into the same recall as memories and each carries its breadcrumb, so results always say where they came from. | Kind | Extensions | | ----- | --------------------------------------------------------- | | Text | `.md` `.txt` `.pdf` | | Web | any `http(s)` URL | | Audio | `.mp3` `.m4a` `.wav` `.flac` `.ogg` `.opus` `.aac` `.wma` | | Video | `.mp4` `.mkv` `.mov` `.webm` `.avi` `.m4v` | Documents are mirrors of their files: re-adding an unchanged one is a no-op, a changed one replaces the chunks whose text changed and keeps the rest, and a pipeline improvement re-ingests files whose bytes did not change. Linking also starts watching. A folder keeps taking in files that appear in it later, which is what makes pointing a recorder at a folder work. See [Watching files and folders](/guides/file-sync). A file enters two ways: **linked** from disk with this command, or **uploaded** as a snapshot in the viewer. Linking is the stronger identity. Linking a file that was uploaded absorbs the upload; uploading one that is already linked is refused with a pointer to the document. Recordings are transcribed and diarized on this machine through the ingest queue. Run `memloom audio setup` once first. See [Recordings](/guides/recordings). *** ## context list, context remove ```bash theme={null} memloom context list # documents with ids and chunk counts memloom context remove # delete a document and its chunks ``` The file on disk is untouched. An uploaded recording's snapshot in memloom's uploads directory is deleted with it. *** ## context roots, watch, unwatch, forget ```bash theme={null} memloom context roots # watched folders, document counts, last check memloom context unwatch # stop syncing a folder or one file memloom context watch # start again memloom context forget # drop it from the list, keep its documents ``` `watch` and `unwatch` take the path you linked, not an id, and work on a folder or a single file. Nothing already learned is affected either way. `forget` and `remove` read alike and are not the same. `forget` drops a folder from the watch list and keeps every document it produced. `remove` deletes a document and its chunks. `MEMLOOM_SYNC=off` disables watching for the whole daemon. Folders are still recorded, so it picks up where it left off when switched back on. *** ## audio ```bash theme={null} memloom audio setup # fetch the speech and speaker models, once per machine memloom audio models # the catalog: sizes, languages, what is installed memloom audio use # transcribe with a different model from now on memloom audio status # selected model, installed models, ffmpeg ``` Models live in `~/.memloom/models` and are shared across projects. Transcripts are cached by file hash and model, so re-adding a recording or switching back to a model you used before costs nothing. # Daemon Source: https://docs.memloom.dev/cli/daemon serve, stop, ui and init: running the store's single owner ## Overview | Command | Does | | --------------- | --------------------------------------------------- | | `memloom init` | First-run setup, then starts the daemon. Idempotent | | `memloom serve` | Run it in the foreground. Ctrl+C to stop | | `memloom stop` | Stop gracefully and release the store lock | | `memloom ui` | Open the viewer, starting the daemon if needed | Every other command talks to the daemon over HTTP and starts it when needed. *** ## What serve runs | Surface | Port | Notes | | ------------------- | ----- | ---------------------------------------------- | | HTTP API and viewer | 4319 | What every client calls | | Postgres wire | 54329 | Embedded tier only. `MEMLOOM_PG_PORT` moves it | With `MEMLOOM_PG_URL` set the daemon connects to your Postgres and skips the wire bridge. If the wire port will not bind, it says so and serves on without it. Config is read from `~/.memloom/config.env` at startup, with real environment variables winning over the file, so restart after editing. The terminal logs every request twice, on arrival and on completion with status and duration, and warns when a wire client connects and pauses the API. One command needs the daemon stopped, because it opens the store directly: [`reembed`](/cli/graph). ## init ```bash theme={null} memloom init ``` Creates `~/.memloom` with a commented `config.env` and starts the daemon. Set `OPENROUTER_API_KEY` there for real embeddings, dedup and entity extraction, then restart. # Graph and embeddings Source: https://docs.memloom.dev/cli/graph index, reembed, auto-index and schema ## index ```bash theme={null} memloom index [--rebuild] ``` Extract entities from unindexed memories and chunks into the graph. One model call per item, so it needs a key. With auto-index on this usually reports nothing pending. `--rebuild` wipes extracted entities and their edges, then re-extracts from scratch. It does not touch memories, conflicts, or the `replaces` and `distinct` edges that conflict resolution created. *** ## reembed ```bash theme={null} memloom reembed [--force] ``` Recompute every embedding with the currently configured provider, then stamp the new fingerprint. Run it after switching provider or model, for example after adding a key to leave offline mode. Without it the daemon refuses to start, because old and new vectors live in incompatible spaces. Stop the daemon first. `reembed` opens the store directly. Interrupting is safe: only vectors are touched and running again resumes where it stopped. Costs one call per 64 items. `--force` re-embeds even when the store already matches the config. A typical offline to cloud migration: ```bash theme={null} memloom stop # set OPENROUTER_API_KEY in ~/.memloom/config.env memloom reembed memloom serve memloom index # build the graph, which offline mode could not ``` *** ## auto-index ```bash theme={null} memloom auto-index # show memloom auto-index off # index only on demand ``` Background extraction a few seconds after new memories and files land, debounced into batched runs and visible in the Console. The setting persists across restarts; `MEMLOOM_AUTO_INDEX` is only the default before the first use of this switch. Needs a key, so offline cannot enable it. *** ## schema ```bash theme={null} memloom schema [list|disable|enable|delete] ``` Manage the extraction vocabulary: the entity types and predicates the model may use. With no arguments, prints it with usage counts and status. | Subcommand | Effect | | ----------------------- | ---------------------------------------------------------------------------------- | | `disable ` | Stops future use. Entities already extracted under it stay. Works on built-ins too | | `enable ` | Re-enables a disabled entry | | `delete ` | Removes a **disabled** entry you added. Built-ins can only be disabled | `` is `entity_type` or `predicate`. How the model proposes new entries for your approval is covered in [The graph](/concepts/graph). # Import Source: https://docs.memloom.dev/cli/import Distill your agent's sessions into memories ## Overview Your agents already know things: months of decisions in Claude Code transcripts, and the notes they write to disk. Import brings both into the store, through the same belief pipeline as anything else. Distills Claude Code transcripts into typed memories. Spends model calls. Reads the memory files agents already keep. No extraction, so free. Same pipeline, triggered as each session ends. Start with `--dry-run` on either import. It lists what would be processed and makes zero model calls. *** ## import sessions ```bash theme={null} memloom import sessions --dry-run memloom import sessions ``` Each session is read from `~/.claude/projects`, scrubbed of secret-shaped strings, distilled into facts, preferences, episodes and procedures, and saved. Duplicates merge, restatements version, contradictions become reviewable conflicts. Every memory keeps provenance: the session file, the line range, and a short excerpt. See [Session distillation](/concepts/distillation). | Flag | Does | | -------------- | --------------------------------------------------- | | `--dry-run` | List sessions and chunk counts. No calls, no writes | | `--days N` | Widen the day window. Default 14 | | `--sessions N` | Raise the session cap. Default 20 | | `--project X` | Only projects whose folder name contains X | | `--agent X` | Which agent. Today only `claude-code` | | `--force` | Reprocess everything, ignoring the ledger | Bounded by default and never silent: anything skipped is announced. Re-running is cheap, because a ledger tracks how far each session got. An unchanged session is skipped, a grown one has only its new part distilled, and a rewritten one is reprocessed from the start. The run ends with a cost line: extraction calls, dedup calls, and embedding batches. *** ## import agent-memory ```bash theme={null} memloom import agent-memory --dry-run memloom import agent-memory ``` Claude Code writes one markdown file per memory under each project's `memory/` folder, and Copilot keeps topic files whose `##` sections are individual memories. Those are already distilled, so this needs no extraction: each is redacted, embedded and saved with provenance. | Flag | Does | | ------------- | ------------------------------------------------------ | | `--agent X` | `claude-code` or `copilot`, repeatable. Default both | | `--dry-run` | List folders and counts | | `--project X` | Only Claude Code projects matching X | | `--force` | Re-import everything, ignoring the content-hash ledger | This is an import, not a sync. An edited agent file re-imports and versions the memory, but deleting one from the agent's folder does not delete it here. memloom never writes into those folders. *** ## Automatic capture ```bash theme={null} memloom connect claude-code --project myapp memloom status ``` A session-end hook hands the daemon a path the moment a session finishes, and the daemon distills it in the background with the same pipeline. The hook exits in milliseconds. If the daemon is down the next start sweeps the gap from the ledger, so nothing is lost. Capture scope is an allowlist and it is **empty by default**. Nothing is captured until you name projects with `--project`, or opt into `--all`. The hook fires for every project on your machine, including client work whose code should never reach a provider. It merges with your existing hooks and never replaces them, a one-time backup is written before the first edit, running `connect` twice changes nothing, and `memloom disconnect claude-code` removes only memloom's entries. A settings file that does not parse is refused untouched. ### Prompt-time recall `connect` also installs a second hook that recalls relevant memories into every prompt you type, so Claude uses your memory without being told to search it. Recall only reads, so the capture allowlist gates what is imported, not what is recalled. It is built to be invisible when it has nothing to add: it skips slash commands, injects nothing when no memory matches, and treats a stopped daemon or slow response as silence. A hard timeout caps the added latency and any failure exits cleanly. Skip it with `--no-recall`, or remove both hooks with `memloom disconnect claude-code`. Unattended capture runs against a daily distillation budget, so it cannot silently burn credits. `memloom status` shows which hooks are installed, when they last fired, whether the last capture failed, and whether the budget is spent. *** ## Privacy Transcripts never leave your machine except as redacted chunks sent to the provider you configured. Redaction is pattern-based and best-effort, so review with `--dry-run` and the viewer if your sessions handle sensitive material. Subagent transcripts and compaction summaries are excluded, and import refuses to run offline rather than degrade silently. # Memories Source: https://docs.memloom.dev/cli/memories save, recall, update and history: the memory lifecycle from the terminal ## save ```bash theme={null} memloom save "the staging database runs on Postgres" memloom save --type procedure "to release: bump VERSION, tag, push" ``` With a key configured the belief pipeline runs: a duplicate merges, a rewording versions, and a contradiction keeps both memories active and reports a conflict id. | Type | Holds | Example | | ------------ | ------------------------ | ---------------------------------- | | `fact` | A stable truth | *the staging DB runs on Postgres* | | `preference` | How you like things done | *prefers pnpm over npm* | | `episode` | A time-bound event | *shipped the viewer on 2026-07-05* | | `procedure` | Reusable steps | *to release: bump, tag, push* | Outcomes are `added`, `merged`, `versioned` or `conflict`. A conflict is not an error. Both memories stay active until you settle it. See [Conflicts](/cli/conflicts). *** ## recall ```bash theme={null} memloom recall "staging database" memloom recall "ECONNREFUSED 54329" # exact identifiers work well ``` Searches memories and ingested files together, by meaning, exact words and entities, fused into one ranking. Results from files carry their source. Only active memories come back; superseded versions live in history. See [Retrieval](/concepts/retrieval). *** ## update, history ```bash theme={null} memloom update "the corrected text" memloom history ``` `update` writes a new version and leaves the old one in history. `history` prints the whole chain newest first, with `*` on the current version. Any version's id works, so an id from an old note still finds the chain. # Notion Source: https://docs.memloom.dev/cli/notion Sync Notion pages and databases as recallable context ## Overview The connector syncs pages you choose into memloom as context documents, recallable alongside your memories and files, and keeps them fresh. No extraction, so sync costs only embeddings, and only for content that changed. At [notion.so/profile/integrations](https://notion.so/profile/integrations). Page menu, then Connections. Access recurses, so sharing a page includes its subpages and database rows. `NOTION_TOKEN=...` in `~/.memloom/config.env`, then restart the daemon. *** ## notion connect ```bash theme={null} memloom notion connect # interactive picker memloom notion connect --page Diary # by title or id, repeatable memloom notion connect --all # everything the integration can see ``` The picker shows the workspace as a tree. Subpages and databases indent under their parent, and database rows collapse into a row count. Syncing a database captures every row; a specific row with body content is still selectable with `--page`. *** ## notion sync ```bash theme={null} memloom notion sync memloom notion sync --dry-run # list what would sync, no content fetches memloom notion sync --force # ignore every cache and refetch in full ``` Pages become markdown documents: toggles flattened, to-do state kept, tables as pipe rows. The daemon also polls every 5 minutes, so edits land without you running anything. | Level | How it saves work | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Change detection | Asks each page directly rather than trusting Notion's search index, so a fresh edit is never missed. An idle workspace costs one call per poll | | Section refetch | Only sections whose `last_edited_time` moved are re-downloaded. Editing one day in a 494-day diary refetches one section | | Chunk reuse | Unchanged chunks keep their row, embedding, indexed state and entity links | A page-level change that no section accounts for falls back to a full fetch, so nothing slips through. A manual sync arriving mid-poll waits its turn rather than being refused. *** ## notion status and disconnect ```bash theme={null} memloom notion status # token, selection, last sync, synced documents memloom notion disconnect # stop syncing ``` Disconnect clears the selection and stops polling. Documents already synced stay in the store and stay recallable. Remove them like any context document. The token never crosses memloom's HTTP API or the viewer; the daemon reads it from its own environment. Page content goes only to your embedding provider, and only the changed parts. Notion webhooks need a public HTTPS endpoint, so a local-first daemon polls by design. # Overview Source: https://docs.memloom.dev/cli/overview Every memloom command, and how the CLI relates to the daemon ## Overview ```bash theme={null} npm install -g memloom # Node 20 or later memloom init # creates ~/.memloom, starts the daemon ``` The CLI is a thin client. Every command talks to the daemon over HTTP on `127.0.0.1:4319` and starts it if it is not running, so the CLI, agents and the viewer share one store with no lock conflicts. `npx memloom@latest `. Pin `@latest` so npx does not reuse a stale copy. `memloom help` lists everything. `memloom help save` explains one. `~/.memloom/config.env`. Set `OPENROUTER_API_KEY` and restart for cloud mode. *** ## Commands | Command | Does | Page | | --------------------------------------- | --------------------------------------------- | -------------------------------- | | `init` `serve` `stop` `ui` | Set up, run, stop the daemon, open the viewer | [Daemon](/cli/daemon) | | `save` `recall` `update` `history` | Write and read memories | [Memories](/cli/memories) | | `context add` `list` `remove` | Ingest files and folders | [Context](/cli/context) | | `index` `reembed` `auto-index` `schema` | Build and manage the graph | [Graph](/cli/graph) | | `conflicts` `conflicts auto` | Settle contradictions | [Conflicts](/cli/conflicts) | | `reconcile` and its subcommands | The pass over your own store | [Reconciliation](/cli/reconcile) | | `import sessions` `import agent-memory` | Bring in what agents already know | [Import](/cli/import) | | `notion` | The Notion connector | [Notion](/cli/notion) | *** ## Inspecting the store directly On the embedded tier the daemon also speaks the Postgres wire protocol. Point Drizzle Studio, psql or any client at: ``` postgresql://postgres@127.0.0.1:54329/postgres ``` `MEMLOOM_PG_PORT` moves it, and `memloom serve` prints the live one. With `MEMLOOM_PG_URL` set there is no bridge, so connect to your server directly. PGLite takes one connection at a time. While a database client is attached, memory endpoints answer 503. Disconnect to resume. # Reconciliation Source: https://docs.memloom.dev/cli/reconcile reconcile and its subcommands: the pass, its findings, and how to take a run back ## Overview | Command | Does | | -------------------------------- | --------------------------------------------------- | | `reconcile [--dry-run]` | Run the pass, or preview it | | `reconcile possible` | List the unconfirmed contradictions it found | | `reconcile yes ` / `no ` | Confirm one into a conflict, or dismiss it for good | | `reconcile stop ` | End a run that is still going | | `reconcile undo ` | Put back exactly what one run did | | `reconcile settings` | Print which passes are on | What each pass does and when a run happens on its own is in [Reconciliation](/concepts/reconciliation). *** ## reconcile ```bash theme={null} memloom reconcile [--dry-run] ``` A bare `memloom reconcile` **acts**. `--dry-run` is the preview. Which passes run comes from your saved settings, so a bare run is the two free passes unless you turned others on. A dry run reports everything, changes nothing, and never calls a model however your settings are set, so it is always free. ``` reconcile (dry run) scanned 412 active memories would retire 2: 9f3c1a04 identical content to 41b0e2d8, which is older and stays active c7d5e910 41b0e2d8 replaced this belief but it is still current entities: would fold 3 name variants (certain) 4 uncertain pairs would go to the conflicts tab 412 memories are due a contradiction re-check re-checking all of them is about $1.45 one run takes the 200 that have waited longest no memory was changed. this run is logged as 0f2c7b18-... ``` The last line always names the run, so undoing one never means looking the id up. A price is only shown for a model whose rates memloom knows. Anything else prints tokens and no dollar figure, rather than a confident wrong number. A run with the contradiction re-check on is minutes of sequential calls and the CLI waits for it. Watch it live in the viewer's Settings tab if you want per-belief progress. Only one applying run at a time; a second is refused while the first is live. *** ## reconcile possible ```bash theme={null} memloom reconcile possible ``` ``` 3 unconfirmed contradictions. about 2 in 5 are real, so read both quotes before answering. similarity 71%, judged by google/gemini-2.5-flash NEW: the release is scheduled for the 14th EXISTING: the release is deferred until the extractor work lands why: deferred then scheduled id: 8a41c0e2-... ``` Both quotes were verified against the two memories when the finding was written, so they are the whole question. Reading them beats opening two beliefs and comparing. ## reconcile yes, reconcile no ```bash theme={null} memloom reconcile yes # confirm: creates a real conflict to resolve memloom reconcile no # dismiss: that pair is never raised again ``` The id comes from `reconcile possible`, not a run id. An id already answered, or one whose memories the store has moved on from, prints a pointer back to the list rather than an error. A rejection is not waste. It records what you do not consider a contradiction. *** ## reconcile stop ```bash theme={null} memloom reconcile stop ``` Whatever the run already checked stays checked, so stopping costs nothing beyond the calls already paid for and the next run resumes there. This also works on a run whose daemon died mid-sweep. That is the only way such a row stops counting as live, and until it does a new applying run is refused. ## reconcile undo ```bash theme={null} memloom reconcile undo ``` ``` restored 4 memories, unfolded 2 entities, skipped 1 the store moved on from ``` | What the run did | What undo does | | --------------------------------- | -------------------------------------------------------------------- | | Retired a memory | Restores it, only while still stale and nothing else staled it since | | Folded an entity | Returns it with its original id and vector | | Raised a conflict nobody answered | Takes it back out of the queue | | Raised a conflict you answered | Leaves it, counted as skipped. Your answer outranks the undo | Possible contradictions are findings rather than changes, so undo leaves them. Undoing twice does nothing the second time. The viewer's Console tab has undo and stop next to the run history. *** ## reconcile settings ```bash theme={null} memloom reconcile settings ``` | Setting | Pass | Cost | | ---------------- | ------------------------------------------------------------- | ----------------------- | | `invariants` | Fix memory principle violations | free, on | | `entities` | Resolve duplicate entities | free, on | | `llm_entities` | Let a model resolve uncertain entity pairs | one call per pair | | `llm_conflicts` | Let a model resolve memory conflicts | one call per conflict | | `llm_recheck` | Look for contradictions the save path could not see | up to 200 calls per run | | `startupCatchUp` | Free passes on startup when the last run is over 36 hours old | free | The CLI prints them and does not change them. Toggle in the viewer's Settings tab or with `POST /memory/reconcile/settings`. They live in the store, not `config.env`, so a change needs no restart. `RECONCILE_ENABLED=0` is a separate kill switch. It refuses any run that was asked to act and stops automatic runs, leaving previews working. See [Configuration](/guides/configuration). # Architecture Source: https://docs.memloom.dev/concepts/architecture One daemon owns the store, everything else is a client ## Overview The default store is PGLite: real Postgres compiled to WASM, embedded in the daemon, with data in a plain folder. PGLite allows exactly one connection, so **`memloom serve` owns the store and every other surface is a client**. ```mermaid theme={null} flowchart LR CLI[CLI] --> HTTP MCP[MCP server] --> HTTP Viewer[Viewer / browser] --> HTTP Studio[Drizzle Studio / psql] --> Wire subgraph daemon ["memloom serve (one process)"] HTTP["HTTP API"] --> Engine Wire["Postgres wire"] --> Store Engine["@memloom/core"] --> Store["PGLite, ~/.memloom/data"] end Engine -.-> Providers["embeddings + LLM (OpenRouter, or offline)"] ``` The daemon exposes the store two ways from one process: the HTTP API on `127.0.0.1:4319`, which also serves the viewer, and the raw Postgres wire on `127.0.0.1:54329` for database tools. Any CLI command starts it if it is not running. A connected wire client holds an exclusive lock, so every API call would queue behind it. The API detects this and answers **503** within about 1.5 seconds instead of hanging. Disconnect the database client to resume. *** ## Packages | Package | Role | | ----------------- | ------------------------------------------------------------------------------------------------------- | | `@memloom/core` | The engine. Belief pipeline, retrieval, graph, ingestion, migrations. No HTTP, no CLI, no `process.env` | | `@memloom/server` | A thin Hono layer over the engine. Validates requests, serves the viewer | | `memloom` | Commands and the daemon. Wires storage, config and providers together | | `@memloom/mcp` | The [MCP tools](/mcp/tools) for agents | | `@memloom/viewer` | The React UI, baked into the CLI package so no separate process is needed | *** ## The engine contract Every surface programs against one interface. ```ts theme={null} interface MemoryEngine { save(input): Promise; recall(query, opts?): Promise; memories(): Promise; update(input): Promise; history(memoryId): Promise; index(): Promise; graph(): Promise; conflicts() / resolveConflict() / revertConflict(); contextAdd() / contextList() / contextChunks() / contextRemove(); } ``` Two implementations: `Memloom` in-process, and `HttpMemloomClient` over HTTP. The CLI and MCP use the HTTP one, which is why many clients share one store safely. They were never holding it. *** ## Storage tiers | Adapter | Tier | Selected by | | --------------- | -------------------------------------------- | ---------------- | | `PgliteAdapter` | Embedded, zero setup, data in a folder | Default | | `PgAdapter` | A real Postgres server with pgvector, pooled | `MEMLOOM_PG_URL` | The schema is identical. Migrations are plain SQL and the one stored function is `LANGUAGE sql` rather than plpgsql, so it runs the same on both. See [Self-hosting](/guides/self-hosting). *** ## Rules worth knowing Core never reads `process.env`. The daemon is the only place config is read, then everything is passed in. That is why the engine is testable without a network. Superseded memories go stale and edges deactivate rather than being deleted. That is what makes versioning and conflict resolution reversible. One edge table links memories, entities and chunks, which are different node tables. Every delete path removes its own edges explicitly. A store embedded with one model refuses to open under another, because mixed vector spaces make similarity silently meaningless. ## Data layout ``` ~/.memloom/ (override with MEMLOOM_HOME) ├── config.env settings the daemon reads at startup └── data/ your memory. Copy it to back it up. ``` # The assistant Source: https://docs.memloom.dev/concepts/assistant Chat with your store: grounded answers, cited sources, any OpenRouter model ## Overview `memloom ui` has an assistant tab that answers from your memories and ingested files, and shows which passages it used. It is told to answer personal questions only from what recall returns, to say plainly when your store has nothing, and never to invent a memory. Ask "how does the ingest worker restart?" and it pulls the runbook section you ingested last month, answers from it, and cites it. Ask "what day is it?" and it just answers. *** ## How a turn works The model sees your message, the history, and two tools: `recall_memory` and `read_source`. Small talk and general knowledge get a direct answer with no retrieval. The query runs through the same [hybrid retrieval](/concepts/retrieval) as `memloom recall`. Day-specific questions add a calendar filter. Passages are injected up to 8,000 characters. When the answer may live in a truncated part, the model calls `read_source(n)`. It can reformulate and search again, capped at three calls, and cannot repeat a query. `[n]` markers follow sentences that used passage `n`. Markers pointing at passages that do not exist are stripped server-side, so every citation is real. Every answer carries its sources: each passage with its similarity, its type, the exact text the model saw, and a jump into the graph. Retrieved passages are data, not instructions. A document saying "ignore your instructions" is quoted like any other text. The assistant has no write tools, so the worst a hostile document can do is word an answer badly. *** ## Picking the model The composer lists every **tool-capable** model on OpenRouter, fetched live, grouped by provider, with prices and context windows. Tool calling is the filter because the assistant grounds itself by calling `recall_memory`. Your choice lives in the browser and rides each request. `OPENROUTER_CHAT_MODEL` stays the default. Nothing restarts when you switch, and the store underneath is the same. *** ## Attaching files to a chat The **+** button uploads `.md`, `.txt` or `.pdf` into the current chat. They go through the normal pipeline but stay scoped to that session. | | | | ------------------------- | ---------------------------------------------------------- | | **Searchable there only** | Fused into that chat's recall. Other chats never see them | | **Not permanent** | Out of the documents tab, the graph, and entity extraction | | **Deleted with the chat** | Removing a chip removes one file | For a file you want to keep, use the documents tab instead. ## Sessions Chats persist in the store. The sidebar has rename, star, delete, and search: keyword over titles and messages, plus embedding similarity, so "that chat about the launch window" finds itself even when no message contains those words. The assistant needs a chat-capable model. Without `OPENROUTER_API_KEY` the tab explains what to configure instead of pretending to work. Everything else in the viewer keeps running. # Conflicts Source: https://docs.memloom.dev/concepts/conflicts Contradictions are kept, surfaced, and resolved by you, reversibly ## Overview When an incoming memory cannot be true at the same time as an existing one, memloom keeps both active and records a conflict. Nothing is auto-overwritten and nothing is lost while it is pending. No last-write-wins, no model picking a winner behind your back. Four resolutions, from the CLI, the viewer, or an agent relaying your choice. Losers go stale, never deleted. Change your mind a week later and resolve it the other way. *** ## How one is detected On every save with dedup enabled: Matched by content hash, with no model call. Up to 5 active memories at cosine similarity 0.5 or above. The model says how it relates to the incoming memory. | Relation | Meaning | Result | | --------------- | ------------------------ | --------------------------------------- | | `identical` | The same fact restated | New **version** of the existing belief | | `complementary` | Both can be true | Both coexist | | `contradictory` | They cannot both be true | **Conflict recorded**, both stay active | The record keeps the incoming memory, every contradicting candidate, and the classifier's stated reason, so you can see later why it was flagged. Detection needs a model, so it is off in offline mode. Offline saves skip the belief pipeline and always report `added`. *** ## Resolving Pending conflicts appear in `memloom conflicts`, the viewer's Conflicts tab, `GET /memory/conflicts`, and the `list_conflicts` MCP tool. | Action | Effect | | ------------------ | ----------------------------------------------------------------------------------------------- | | **keep\_new** | The incoming memory continues the existing lineage as its next version. The old one goes stale | | **keep\_existing** | The incoming memory goes stale, the existing one stays current | | **keep\_both** | Genuinely distinct. Both stay active, linked `distinct` so they are never re-flagged | | **merge** | You write the sentence that is actually true. It becomes the next version, both inputs go stale | Revert reactivates whatever was staled, deactivates the edges the resolution created, undoes any lineage re-parenting, and puts the conflict back in the queue. *** ## Letting a model clear the queue ```bash theme={null} memloom conflicts auto ``` Runs the same model over the pending queue a second time, now with context the save-time classifier never had: when each side was recorded, and for imported memories the transcript excerpt it came from. Most import conflicts are a later session recording a change ("we moved to railway") against an older fact ("the deploy target is fly.io"), which is mechanical once you can see the dates. It answers `keep_new`, `keep_existing`, `keep_both`, or `unsure`, and is told to prefer `unsure` over guessing. Only a decisive verdict resolves anything, and it lands in the same revertable history as a manual one. `merge` is not available to the judge. There is no reconciled text for a model to author on your behalf. *** ## Three queues, one tab The Conflicts tab holds three kinds of question. They share a surface and a revert, but not a scope. | Queue | What it is | In `list_conflicts`? | | --------------------------- | ----------------------------------------------------------------------------------------- | -------------------- | | **Memory conflicts** | Two memories that cannot both be true | Yes | | **Duplicate entities** | Two spellings that may be one thing. See [Entity resolution](/concepts/entity-resolution) | No | | **Possible contradictions** | Unconfirmed findings from [reconciliation](/concepts/reconciliation) | No | Only memory conflicts reach the CLI and MCP surfaces. An agent asking about pending conflicts is never handed a question about spelling, or a finding no human has confirmed. Save-time detection compares an incoming memory against the 5 nearest that existed at that moment, so a pair at rank 6 is never judged. The re-check goes back over old beliefs against 20 neighbours and finds those. Its measured precision is roughly 40 percent, so they wait as findings where dismissing costs a click. Each shows the verbatim clashing sentence from both sides, so it can be judged in seconds. Confirming one writes the real conflict row, and from there it behaves like anything else on this page. # Session distillation Source: https://docs.memloom.dev/concepts/distillation How a Claude Code session becomes memories ## Overview Every Claude Code session is a JSONL file under `~/.claude/projects`. Both `memloom import sessions` and `memloom connect claude-code` turn them into memories through the same pipeline. They differ only in trigger: one runs on demand over past sessions, the other installs a session-end hook so each session is distilled as it finishes. Secret-shaped strings are scrubbed before anything leaves your machine. Distilled memories dedup, version and conflict exactly like hand-saved ones. Every memory keeps the session, the line range, and the excerpt it came from. *** ## The pipeline UUID-named main transcripts only, inside `--days` (14) and `--sessions` (20). Subagent sidecars are skipped. A file touched in the last 5 minutes is still being written, so it waits for the next run. Anything left out is reported. User and assistant text only. Tool calls, tool results, subagent turns, meta lines and compaction summaries are dropped before the model sees them. Line numbers are kept. A regex scrub for JWTs, vendor key formats, `KEY=value` assignments and credentialed URLs. Chunks of about 24,000 characters, one model call each, returning typed memories with a line range. Output that does not parse into a known type is dropped and counted. Through the normal belief pipeline, with a provenance row recording where it came from. Redaction is pattern-based and best-effort. Review sensitive sessions with `--dry-run` and the viewer rather than treating it as a hard boundary. The distiller treats the transcript as untrusted data. Instructions inside a fetched page or a tool's output are text a transcript happens to contain, not instructions to follow. *** ## Conflicts get one extra step Distilled memories go through the same [belief pipeline](/concepts/conflicts) as anything else. Import adds one thing: because a distilled memory carries its transcript excerpt, a contradiction is judged once, immediately, by a model that can read that excerpt. | Verdict | Result | | --------- | ---------------------------------------------------- | | Decisive | Resolved on the spot, revertable like any resolution | | Ambiguous | Left pending in `memloom conflicts` and the viewer | Nothing bypasses your review. It only saves you from resolving conflicts the transcript already answered. *** ## Re-running is cheap A ledger tracks how far each session was processed: a line offset and a content hash. | State | What happens | | ------------- | ----------------------------------------------------------------------- | | Unchanged | Skipped. One ledger lookup, no model call | | Grew | Resumes from the watermark, re-reading the last 40 lines for continuity | | Hash mismatch | The file was rewritten, so it is reprocessed from the start | *** ## Continuous capture ```bash theme={null} memloom connect claude-code ``` The same pipeline, triggered by a session-end hook that hands the daemon one file path the moment Claude Code exits, skipping discovery. A daily cap on unattended distillation means a runaway loop cannot quietly spend your budget, and `memloom status` says so when the cap is hit. See [Import](/cli/import). # Entity resolution Source: https://docs.memloom.dev/concepts/entity-resolution Fold the several spellings of one thing into one entity, reversibly ## Overview Extraction echoes whatever spelling the text used, so a graph accumulates `Postgres` and `PostgreSQL`, `Claude Code` and `claude-code`, as separate entities. Recall anchors a query to at most 10 entities, so every duplicate spends a slot naming something another slot already names. Rules find the pairs. A model only ever confirms one, and never proposes one. One rule folds without asking. Everything else becomes a question in the Conflicts tab. A merge record stores every edge that moved, so revert restores the original row exactly. *** ## What a fold does One entity becomes the **canonical** and the other moves into it: * Every mention and relationship repoints to the canonical. * The absorbed spelling becomes an **alias**, keeping the original id and vector. * A **merge record** stores which edges moved and which were deactivated. Nothing is deleted. The alias is also what extraction looks up on the next mention, so a new memory saying `Opus 4.8` lands on the canonical instead of minting the row again. *** ## How pairs are judged Entities are bucketed by their words and key prefix, so only plausible pairs are compared. Each pair gets one verdict. | Rule | Example | Verdict | | ------------------------------------- | ------------------------------- | -------- | | Identical normalized key | `Claude Code` / `claude-code` | **fold** | | Version disagreement | `Opus 4.8` / `Opus 4.7` | reject | | One-sided version | `Postgres 15` / `Postgres` | reject | | Extension or domain | `memloom.ts` / `memloom` | reject | | Contained phrase, two or more words | `Opus 4.8` in `Claude Opus 4.8` | ask | | Spelling variant within edit distance | `Postgres` / `PostgreSQL` | ask | The identical-key rule casefolds and drops non-alphanumerics, so `ClaudeCode` collapses too. Two escapes send it to review instead: keys under 3 characters (`C#` collides with `C`), and punctuation-prefixed names (`.next` is a directory, `Next` is a framework). People are exempt from the two-word requirement, since a human's name is written at several lengths: `Ada` pairs with `Ada Lovelace`. Both sides must be typed `person`, and it still only asks, because `John` against `John Smith` and `John Doe` is a real question. *** ## Running it ```bash theme={null} POST /memory/entities/resolve {"dryRun": true} # what it would do POST /memory/entities/resolve {} # do it ``` Idempotent and safe to re-run. | Field | Meaning | | ---------- | ----------------------------------- | | `examined` | Entities considered | | `pairs` | Pairs judged | | `merged` | Folded without asking | | `queued` | Written to the review queue | | `deferred` | Held back because the queue is full | | `skipped` | Already merged, queued, or settled | The queue caps at 50 pending questions so entity folds cannot bury memory conflicts. `deferred` is there so "nothing queued" never hides "there was more to ask". *** ## Reviewing the questions Uncertain folds land in the [Conflicts](/concepts/conflicts) queue, in the same place and with the same reversibility as a contradiction between two memories. | Action | Effect | | ------------------ | ------------------------------------------- | | **keep\_existing** | Fold the queued spelling into the candidate | | **keep\_new** | Fold the candidate into the queued spelling | | **keep\_both** | Different things, stop asking | On an entity, `keep_both` means "settled, do not ask again". The resolver also writes it when a question became moot, so the decision log is not purely a record of human judgement. *** ## Reverting ```bash theme={null} POST /memory/entities/merges/{id}/revert ``` Restores the absorbed entity with its original id, type, vector and creation time, repoints exactly the edges the merge moved, and removes the alias. The original id matters because deactivated edges still point at it. Deleting an entity that an unreverted fold points at is refused. The alias carries the absorbed entity's id and vector, so deleting the canonical would make the fold permanent. Revert first, then delete. *** ## Asking who is connected to what ```bash theme={null} GET /memory/entities/related?q=Bob&type=person ``` The target can be an id, a name, or a folded-away spelling. Two kinds of connection come back separately and are never blended into one score: * **Stated** relationships are typed edges extraction asserted, with direction and confidence. * **Co-mention** is entities named by the same sources, with a count. Weaker, so it never outranks a stated link. In the viewer this is the traverse panel on the Graph tab. Agents use [`related_entities`](/mcp/tools). ## What it will not do A nickname sharing no letters with the name, `Bob` against `Robert`, is never proposed. That is not a lexical judgement. You can still declare it yourself with `POST /memory/entities/{id}/merge`, which records the same reversible merge. # The graph Source: https://docs.memloom.dev/concepts/graph One graph over memories and context, at two granularities ## Overview Memories and ingested documents live in one graph, connected by the entities they mention. Your saved belief "the ingest worker restarts via systemd" and the runbook you ingested both mention **ingest worker**, so they sit two hops apart, and recall follows the same edges. Entity edges point at individual chunks, so the engine knows which section mentions what. The graph API rolls those up into one weighted edge, so a 300-page PDF is one node, not 300. *** ## What is in it | Node | Comes from | In the viewer | | ------------ | -------------------------- | ------------------------ | | **Memory** | Active saved memories | Circle, coloured by type | | **Entity** | `memloom index` extraction | Purple circle | | **Document** | `memloom context add` | Emerald diamond | | **Chunk** | Expanding a document node | Square | | Edge | Meaning | | ---------- | --------------------------------------------------------- | | `mention` | A memory or chunk mentions an entity | | `replaces` | Version lineage: this memory superseded that one | | `distinct` | A resolved *keep both*: related but deliberately separate | Only active memories and edges appear. Stale versions and reverted resolutions drop out. *** ## Indexing Indexing runs automatically a few seconds after you save a memory or add a file, debounced so a folder ingest becomes one run. Saves stay fast because the model work never blocks them. ```bash theme={null} memloom auto-index off # make indexing explicit again memloom index # run it now memloom index --rebuild # wipe extracted entities and start over ``` Extraction is precision-first: it would rather miss an entity than invent one. One model call per row against a closed vocabulary of 9 entity types, capped at the 5 most salient entities. The prompt lists names already in your graph so the model reuses them. A deterministic pass drops out-of-vocabulary types, math expressions, sentence fragments, and names under two letters. Formula-heavy chunks skip the model entirely. Survivors resolve to a node by name key: casefolded, trimmed, leading `@` dropped. Folded-away spellings resolve too, so `Opus 4.8` lands on `Claude Opus 4.8`. Entity types: `person`, `organization`, `project`, `tool`, `technology`, `agent`, `place`, `event`, and `concept`, which is explicitly a last resort. `agent` means a named AI model or assistant that does work, the actor rather than the company that made it. The same call extracts typed relationships against a closed predicate list: `uses`, `depends_on`, `part_of`, `created_by`, `works_on`, `works_at`, `located_in`, `attended`. Out-of-vocabulary predicates and anything under 0.7 confidence are kept as plain `mention` edges. A node's identity is its name key alone. Type is an attribute, so an extractor that says `technology` today and `project` tomorrow cannot fork the node. Extraction is not deterministic: the same input can extract differently on another run. `--rebuild` also needs a model, so running it offline still wipes, leaving everything unindexed until a key is configured. Belief edges are never touched by a rebuild. *** ## The schema The vocabularies live in a registry, not in code. Edit them in the viewer's Schema tab, with `memloom schema`, or at `GET /memory/schema`. | Action | Effect | | ----------- | ------------------------------------------------------------------- | | **Add** | A type or predicate of your own joins the prompt on the next run | | **Propose** | The extractor asks for a missing type. Approve or dismiss it | | **Disable** | Leaves the prompt. Entities already extracted under it stay | | **Delete** | Only for disabled entries you added. Built-ins can only be disabled | The description you write **is** the extraction rule, so write it like one: "a named drug or supplement" rather than "medications". A proposal only surfaces after two independent extractions asked for it. *** ## Managing entities The Schema tab lists every entity with its usage counts and the tools to correct it. | Action | Notes | | -------------- | ---------------------------------------------------------------------------------------------- | | **Rename** | Re-embeds under the new name. Renaming onto an existing name is refused, since that is a merge | | **Retype** | To any active vocabulary type | | **Merge into** | Moves every mention and relationship to the survivor, keeps an alias, fully revertable | | **Delete** | Removes the entity and its edges. Refused while an unreverted fold points at it | memloom can find the duplicates for you. See [Entity resolution](/concepts/entity-resolution). *** ## Traversal ```bash theme={null} GET /memory/entities/related?q=&type= ``` Walks out from one entity instead of returning the whole graph. Stated relationships come back separately from entities that merely co-occur, because those are different kinds of evidence. In the viewer it is the traverse panel on the Graph tab; agents use [`related_entities`](/mcp/tools). ## In the viewer Click a document diamond to expand it into chunks with their real chunk-level edges. Click a chunk to read it, with breadcrumbs back up. A document's panel has an **Open file** button. The daemon only opens files it previously ingested, looked up by document id and never by a path from the request, so that button cannot be pointed at arbitrary files. # Memory types and versioning Source: https://docs.memloom.dev/concepts/memory-types The taxonomy, the belief lifecycle, and version chains ## The taxonomy Every memory carries a type, the same one across CLI, MCP and HTTP. Defaults to `fact`. | Type | Holds | Example | | ------------ | ------------------------------ | --------------------------------------- | | `fact` | A stable truth | *the staging database runs on Postgres* | | `preference` | How you like things done | *prefers pnpm over npm* | | `episode` | A time-bound event or decision | *shipped the viewer on 2026-07-05* | | `procedure` | Reusable steps | *to release: bump VERSION, tag, push* | A database constraint rejects anything else, even if it gets past the API. Choosing well pays off twice: the viewer filters by type, and the MCP tool description teaches agents the same definitions, so an agent saving "user prefers tabs" tags it `preference` unprompted. Recall results can also carry the sentinel type `context`. Those are chunks of ingested files, not saved memories, and their `kind` is `context`. You cannot save one. The sentinel exists so a single result list can hold both kinds honestly. *** ## Beliefs, not rows A memory is a **belief with a lineage**. Rows are never mutated in place. `rootId` is the stable identity of the belief, `version` counts up from 1. The newest active row is the belief. Older ones are stale: kept forever, out of recall, dedup and the graph. Each step writes a `replaces` edge from child to parent, so history and the graph tell the same story. `assertedAt` is when a version became the belief. `staleSince` is when it stopped. *** ## What creates a version | You do | memloom decides | Outcome | | ---------------------------- | ------------------------------- | ------------------------------- | | Save the same text again | Content hash matches | `merged`, a no-op | | Save the same fact, reworded | Classifier says *identical* | `versioned`, old one goes stale | | Save a fact that can coexist | Classifier says *complementary* | `added`, its own lineage | | Save a contradiction | Classifier says *contradictory* | `conflict`, both stay active | | `memloom update ` | No classifier, explicit edit | New version, unconditionally | Resolving a conflict can step a version too: **keep new** and **merge** make the winner continue the existing lineage, so a settled contradiction lands in that belief's history instead of orphaning it. *** ## Walking history ```bash theme={null} memloom history 43c1 # * v3 2026-07-10 the staging db runs on Postgres 17 # v2 2026-07-02 the staging db runs on Postgres 16 # v1 2026-06-20 staging uses Postgres ``` Any version's id works. The same chain is at `GET /memory/:id/history`, in the viewer's memory panel, and in the read-only `memory_history` MCP tool. Agents can read history but not edit a belief. `update` is deliberately a human action in the CLI or viewer. Recall never returns stale versions, so old ones cannot shadow the current one. # Reconciliation Source: https://docs.memloom.dev/concepts/reconciliation memloom reviews its own store, fixes what it can prove is wrong, and asks you about the rest ## Overview A store that only grows drifts. Duplicates land, resolutions leave loose ends, and beliefs written months apart contradict each other with nothing there to notice. Reconciliation is the pass that looks. Every run is one unit with an id. `memloom reconcile undo ` takes the whole thing back. The two passes that are on can only do what undo reverses. The three that cost money stay off until you turn them on. Automatic runs use the free passes only. Money is spent from a command or a button, never a timer. *** ## The five passes Cost order, the same order the CLI report and the Settings tab use. | Pass | Cost | Default | | --------------------------------------------------- | ----------------------- | ------- | | Fix memory principle violations | free | **on** | | Resolve duplicate entities | free | **on** | | Let a model resolve uncertain entity pairs | one call per pair | off | | Let a model resolve memory conflicts | one call per conflict | off | | Look for contradictions the save path could not see | up to 200 calls per run | off | | Pass | Repairs | Reports only | | ---------------------- | ----------------------------------------------------------- | -------------------------- | | Memory principles | Duplicate content, memories left current after a resolution | Orphans, entity invariants | | Duplicate entities | Name variants the rules are certain about | Pairs that need a human | | Uncertain entity pairs | Folds the queue a model can settle | Leaves the rest queued | | Memory conflicts | Re-judges the pending queue | Leaves unsure ones pending | | Contradiction re-check | Nothing. Every finding waits for you | All of them | Entity invariants are reported and never repaired. They mean something upstream wrote a bad row, and auto-fixing that hides the bug instead of fixing it. *** ## The contradiction re-check A save is judged against the **5 nearest** memories that existed at that moment. A pair sitting at rank 6 is never compared by anything, and nothing looks again. On a 3000-belief store that is around 12 unjudged pairs per belief. This pass goes back over old beliefs against their **20 nearest** neighbours. | | | | ------------ | ------------------------------------------------------------------------------ | | Cost ceiling | 200 beliefs a run, about **\$0.55** | | Progress | Per belief, oldest first, so a backlog drains across runs with nothing skipped | | Repeat | A belief comes back around after 30 days | | Precision | About **40 percent** | Because it is wrong more often than right, its findings are **possible contradictions**, not conflicts. They wait in the Conflicts tab. Confirming one creates the real conflict; dismissing one is permanent for that pair. The model has to quote the clashing claim from both sides word for word, and the pass drops the finding if either quote is not really there. That is what lets you judge one in three seconds without opening either memory. It is not a precision filter: a real quote can still carry a wrong conclusion. Set a budget in the Settings tab to keep going past the 200 ceiling until the backlog is clear or that much has been billed. *** ## When it runs `memloom reconcile`, or **Reconcile now** in Settings. The only way the paid passes ever run. The daemon starts, settles for two minutes, and runs if the last one is over 36 hours old. Switch it off in Settings. No run for 20 hours and nothing served for 10 minutes, with a 48 hour ceiling for a busy machine. Never while indexing. Automatic triggers run the free passes only, whatever your settings say. A run nobody watched cannot spend money. Per run: 10 retirements, 3 reported questions, 5 conflicts raised, 10 integrity findings. Anything past a cap is held for the next run, and the report says how many. No conflicts are raised at all while 20 are already pending. Every consecutive run whose findings you neither approve nor reject halves the next run's caps, and after three of them a run keeps scanning and stops talking. Integrity findings are exempt: quieting an opinion is right, quieting an alarm is not. *** ## Undo ```bash theme={null} memloom reconcile undo ``` | What the run did | What undo does | | --------------------------------- | ------------------------------------------------------ | | Retired a memory | Reactivates it, unless something else changed it since | | Folded an entity | Restores the absorbed entity with its original id | | Raised a conflict nobody answered | Removes it from the queue | | Raised a conflict you answered | Leaves it alone and counts it as skipped | Possible contradictions are findings rather than changes, so undo leaves them. Running undo twice does nothing the second time. *** ## Where you drive it | Surface | For | | ------------- | ------------------------------------------------------------ | | Settings tab | The toggles, the budget, Reconcile now, Preview only | | Conflicts tab | The inbox: one item at a time, numbered keys | | Console tab | Every run and its findings, with stop and undo | | CLI | [`memloom reconcile`](/cli/reconcile) | | HTTP | `POST /memory/reconcile` and the rest of `/memory/reconcile` | ## Turning it off ```bash theme={null} RECONCILE_ENABLED=0 ``` In `~/.memloom/config.env`. Reports still run, repairs do not: an applying run is refused with a 403, automatic runs stop acting, and a preview still works. The three paid passes also need `OPENROUTER_API_KEY`, and find nothing without one. # Retrieval Source: https://docs.memloom.dev/concepts/retrieval Hybrid recall: vector, keyword and entity, fused by rank ## Overview Vector search alone misses exact identifiers like `pg_hba.conf`, because embeddings blur precise strings into approximate meaning. Keyword search alone misses the meaning. memloom runs three arms inside the database in a single SQL function and fuses the rankings. One query, one round trip, no reranker service. Saved memories and ingested chunks rank in the same pool, so "search my memories" and "search my files" are one operation. *** ## The three arms | Arm | Signal | Catches | | ----------- | ---------------------------------------------- | -------------------------------------------------------------- | | **Vector** | pgvector cosine distance | Meaning: paraphrases, "the thing about restarting that worker" | | **Keyword** | Postgres full-text search | Exact words: file paths, error codes, config keys | | **Entity** | Query-to-entity similarity, then mention edges | Everything mentioning a thing the query names, however worded | Both the entity and keyword arms **abstain**. The entity arm only fires above 0.60 query-to-entity cosine, so a vague query anchors to nothing rather than dragging in noise. No lexical match means no keyword votes. The entity arm anchors on at most 10 entities per query, which is why duplicate spellings cost real recall quality: each one spends a slot. It also anchors on spellings that [entity resolution](/concepts/entity-resolution) folded away, counting a folded spelling and its canonical once between them. *** ## Fusion Arms are combined by **rank**, not score, because cosine distances and full-text ranks are not comparable. ``` score = Σ over arms: weight / (60 + rank_in_arm) ``` | Arm | Weight | Why | | ------- | ------ | ---------------------------------------------------------------- | | Keyword | 2.0 | Abstains when it has nothing, so its votes are cheap and precise | | Vector | 1.0 | The baseline signal | | Entity | 0.5 | A tiebreaker between arms, not a winner on its own | A result two arms agree on beats one that tops a single arm. Every result carries both numbers: `rrfScore` is the order results arrive in, and `similarity` is the raw cosine on its own. *** ## Getting good results `memloom recall "pg_hba.conf"` is precise in a way pure-vector systems cannot be. The entity arm only sees what `memloom index` processed. The other two work regardless. A chunk stores `Guide > Setup > Postgres` ahead of its text, so both arms see the heading. Every arm filters to active rows, so a superseded version cannot shadow the current one. Offline, the vector arm runs on deterministic hash embeddings, so texts sharing words look similar. That is a real but weak signal. Keyword works at full quality; the entity arm needs indexing, which needs a model. For production recall, [configure a key](/guides/configuration). # Features Source: https://docs.memloom.dev/features What memloom does, in one pass ## Overview One local store, several capabilities layered on it. Every card links to the page that explains the mechanism. A save is not an insert. Duplicates merge, rewordings become versions, contradictions are kept and raised. Nothing is silently overwritten. Meaning, exact words, and entity anchoring, fused into one ranking over your memories and your files together. Chats over your store, answers only from what it found, and cites passages you can open. Markdown, text and PDF, chunked along real structure so a result is one section, not a blind window. Audio and video transcribed and diarized on-device, cited by timestamp. Web pages fetched and parsed in-process. Link a folder and files that appear in it are taken in on their own. An edit re-embeds the section that changed, not the file. Entities link memories and documents. The vocabulary is a registry you can extend, prune, and correct. Folds duplicate spellings, asks about the uncertain ones, and keeps every fold reversible. Every memory is a version chain. Nothing is destroyed, and `memloom history` walks it. When two memories cannot both be true, both are kept and you decide. Every resolution is reversible. The store reviews itself, repairs what SQL proves, and asks about the rest. Automatic runs never spend money. Import past sessions, or capture new ones continuously, distilled through the same pipeline with provenance back to the line range. The store is a folder. Copy it to back it up. The only network calls go to the provider you configure. *** ## Every client, one store | Surface | For | | ------------------------------------- | ----------------------------------------------------------------------------- | | **CLI** | You, in a terminal | | **MCP** | Claude Code, Claude Desktop, Cursor: agents that save and recall as they work | | **Viewer** (`memloom ui`) | Graph, assistant, memories, documents, schema, conflicts, console | | **HTTP** (`127.0.0.1:4319`) | Scripts, editors, anything else | | **Postgres wire** (`127.0.0.1:54329`) | Drizzle Studio or psql against the raw data | All of them talk to one daemon that owns one store, so an agent's save is instantly visible in the viewer and your CLI recall sees what the agent ingested. See [Architecture](/concepts/architecture). # Agent setup Source: https://docs.memloom.dev/guides/agent-setup A runbook an AI agent can follow to install and configure memloom end to end ## For humans: one prompt Paste this into Claude Code, Cursor, or any agent that can run commands and fetch a URL. ```text theme={null} Set up memloom on this machine. Fetch https://docs.memloom.dev/guides/agent-setup and follow it exactly. Ask me before anything that spends money or captures a project. I will give you the API key when you ask. ``` If your agent supports skills: ```bash theme={null} npx skills add memloom/memloom ``` That adds `memloom-setup`, this runbook condensed, and `memloom-use`, how to use memloom well day to day. *** ## For agents: ground rules Everything below is yours to do autonomously, except two things. **The API key.** Ask for `OPENROUTER_API_KEY` when the step comes. If the human declines, continue offline and say that recall quality is degraded until a key is set. Never print the key into logs, chat, or command output. **Consent for capture and paid imports.** Never run `memloom connect claude-code` or a non-dry-run import without the human naming the scope. Report each step briefly. If one fails, check Troubleshooting before improvising. *** ## Steps ```bash theme={null} node --version ``` Expect v20 or later. If older, stop and ask the human to upgrade. npm silently skips the install otherwise. ```bash theme={null} npm install -g memloom memloom init ``` Creates `~/.memloom` and starts the daemon. Expect the config path and `http://127.0.0.1:4319`. Add it to `~/.memloom/config.env`, then restart: ```bash theme={null} OPENROUTER_API_KEY=sk-or-... ``` ```bash theme={null} memloom stop && memloom init ``` If anything was saved before the key was set, the daemon refuses to start, because offline and cloud vectors live in different spaces. Run `memloom stop`, `memloom reembed`, `memloom init`. ```bash theme={null} claude mcp add memloom -- npx -y @memloom/mcp ``` For other clients see the [client pages](/mcp/setup). The shape is always the same: run `npx -y @memloom/mcp` as a stdio server. Ask which projects may be captured, then: ```bash theme={null} memloom connect claude-code --project ``` Repeat per project, or `--all` only with explicit approval. Installs two hooks: session-end capture and prompt-time recall. Both merge with existing hooks, and a backup is written first. ```bash theme={null} memloom import agent-memory --dry-run # free memloom import sessions --dry-run # spends model calls ``` Show the human each result and get approval before running without `--dry-run`. ```bash theme={null} memloom save "memloom setup verified on this machine" memloom recall "setup verified" memloom status ``` Expect the saved memory as the top hit, the same hit from the MCP client's `recall_memory`, and a status showing the daemon running with the chosen capture scope. *** ## Troubleshooting | Symptom | Cause and fix | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `command not found` after install | Node older than 20, so npm skipped the package. Upgrade and reinstall | | Daemon not reachable | Run `memloom init`. Every CLI command auto-starts it except the hooks | | 503 "store is locked" | A Postgres client holds the single-connection lock. Close it | | Refuses to start on an embedding mismatch | `memloom stop`, `memloom reembed`, `memloom init` | | Port 4319 in use | Another daemon is already running. That is fine, every client shares it | | Postgres wire says OFF | Port 54329 will not bind. Set `MEMLOOM_PG_PORT` below 49152 and restart. The API and viewer work either way | | `import sessions` refuses | Distillation needs a model, so set `OPENROUTER_API_KEY` | # Configuration Source: https://docs.memloom.dev/guides/configuration config.env, the two modes, and the embedding fingerprint ## Where config lives ``` ~/.memloom/config.env ``` Created by `memloom init`, dotenv-style, read **at startup**. Restart after editing. Real environment variables win over the file, so a shell or MCP key overrides what is written there. `MEMLOOM_HOME` relocates the whole directory, config and data together. *** ## The settings that matter ```bash theme={null} OPENROUTER_API_KEY=sk-or-... # the one switch: with it, cloud mode. Without it, offline ``` | Setting | Default | For | | --------------------------- | ------------------------- | ---------------------------------------------------------------- | | `OPENROUTER_EMBED_MODEL` | `qwen/qwen3-embedding-8b` | Embeddings | | `OPENROUTER_EMBED_DIMS` | `1024` | Matryoshka truncation from the native 4096 | | `OPENROUTER_LLM_MODEL` | `google/gemini-2.5-flash` | The pipeline's classifier and extractors | | `OPENROUTER_CHAT_MODEL` | same as above | The assistant. Must support tool calling | | `MEMLOOM_AUTO_INDEX` | `on` | Background entity extraction after saves and ingests | | `MEMLOOM_INDEX_CONCURRENCY` | `6` | Extraction worker pool size | | `RECONCILE_ENABLED` | unset | Set to `0` to keep the reports and refuse the repairs | | `MEMLOOM_SYNC` | `on` | Set to `off` to stop [watching linked files](/guides/file-sync) | | `MEMLOOM_PG_URL` | unset | Run on a real Postgres instead of the embedded store | | `MEMLOOM_OWNER_ID` | unset | Which person a process acts for. Environment only, not this file | The full annotated list, including transcription and diarization, lives in `config.env.example` in the repo and in the file `memloom init` writes. `MEMLOOM_OWNER_ID` is the one exception to everything above: it is read from a process's own environment and never from `config.env`. That file configures the daemon, which serves every owner, while this names which owner one client process is, so it belongs to the shell profile or MCP registration that launched it. Unset means the daemon's default owner, which is what a single-owner install has always used. See [memory per person](/guides/memory-per-person). **`MEMLOOM_AUTO_INDEX`** covers the indexing that runs a few seconds after a save or ingest, one model call per new item, debounced. Set it `off` to make every call explicit. `memloom index` still works. Offline never auto-indexes. **`RECONCILE_ENABLED=0`** is a kill switch, not the switch. [Reconciliation](/concepts/reconciliation) is on by default with its two free passes, and which passes run is a viewer setting that needs no restart. Setting `0` makes an applying run fail with a 403 and stops automatic runs acting. Previews keep working. **`OPENROUTER_EMBED_PROVIDER`** exists because embedding latency varies wildly between hosts of the same model: the same call has been seen taking 16 seconds on one and under a second on another. memloom pins the fast host for the default model, so set this only if you change models. *** ## The two modes | | Cloud (key set) | Offline (no key) | | ------------------------------- | ---------------------------- | -------------------------------------------------- | | Embeddings | Real model | Deterministic hash | | Recall | Full semantic | Word overlap, with the keyword arm at full quality | | Dedup and versioning | Yes | No, every save is `added` | | Conflict detection | Yes | No | | `memloom index` | Yes | No, it needs a model | | Context ingestion and citations | Yes | Yes | | Network calls | Embedding and model requests | None | Offline is for trying memloom, tests, and air-gapped keyword search. For real use, set the key. *** ## The embedding fingerprint Vectors are only comparable to vectors from the same provider, model and dims, so the store is stamped on first init, for example `openrouter:qwen/qwen3-embedding-8b@1024`. A later startup with a different fingerprint is refused. This is deliberate. Mixed embedding spaces do not fail with an error, they silently return garbage recall. Changing model is a migration, not a dead end: stop the daemon and run [`memloom reembed`](/cli/graph), which recomputes every vector and stamps the new fingerprint. The LLM model can be changed freely, since it never touches the vector space. *** ## Daemon endpoints | What | Where | Movable | | ------------------- | ------------------------------------------------ | ------------------------------------- | | HTTP API and viewer | `http://127.0.0.1:4319` | No, localhost only by design | | Postgres wire | `postgresql://postgres@127.0.0.1:54329/postgres` | `MEMLOOM_PG_PORT`, embedded tier only | Both bind to `127.0.0.1`, and CORS admits only localhost origins, so a public web page cannot drive your daemon. Hyper-V, WSL and Docker reserve random 100-port blocks inside the ephemeral range at every boot, and one can land on 54329. The daemon then starts without the wire and prints why, so the API and viewer keep working. Set `MEMLOOM_PG_PORT` to a free port below 49152 and restart. `netsh interface ipv4 show excludedportrange protocol=tcp` lists the blocked ranges. # Extractors and chunking Source: https://docs.memloom.dev/guides/extractors How files become searchable chunks, and how to add a format ## Overview ``` file bytes → extract → chunk → embed → store ``` The extractor for the file's extension turns bytes into text units. One unit per page for a PDF, so page numbers survive into citations. Units split along the document's real structure, each chunk carrying a breadcrumb like `Guide > Setup > Postgres`. Embedded in batches before the transaction opens, so the database write stays short and all-or-nothing. Documents are mirrors of files, so the belief machinery does not apply: no dedup, no conflicts, no versions. Re-adding an unchanged file is a no-op; a changed file atomically replaces its chunks. *** ## Built-in extractors | Kind | Extensions | Chunker | Notes | | ------- | --------------------------------------------------------- | -------- | ------------------------------------- | | `md` | `.md`, `.markdown` | markdown | Title from the first heading | | `txt` | `.txt` | outline | | | `pdf` | `.pdf` | outline | Text layer via unpdf, no OCR | | `link` | any `http(s)` URL | markdown | Fetched and parsed locally | | `audio` | `.mp3` `.m4a` `.wav` `.flac` `.ogg` `.opus` `.aac` `.wma` | markdown | Transcribed on this machine | | `video` | `.mp4` `.mkv` `.mov` `.webm` `.avi` `.m4v` | markdown | The audio track, every track mixed in | Recordings have their own page: see [Recordings](/guides/recordings). Web pages that build themselves in the browser are refused with advice rather than stored empty. Text is rebuilt from glyph geometry rather than stream order, so equation-heavy PDFs come out in reading order and 2-up print layouts are de-duplicated. Symbols a PDF's fonts never map to Unicode (∞, ∈, ≠) cannot be recovered from the text layer. That needs OCR, which is deliberately out of scope. *** ## Two chunking strategies Both follow one rule: a chunk never starts mid-section, and always knows where it came from. | | **Markdown** | **Outline** | | --------- | ---------------------------------------------------- | --------------------------------------- | | Splits at | ATX headings, ignoring ones inside code fences | ALL-CAPS titles and numbered points | | Chunk is | One heading section, whole | One definition, theorem or clause | | Size | Up to 16,000 chars, splitting past that toward 8,000 | \~1,600 chars, 2,048 cap, \~200 overlap | | Overlap | None. Heading boundaries are exact | Yes, carried across boundaries | Every chunk gets its breadcrumb prepended to the text, so both the vector and keyword arms see the section context along with the paragraph. Text with no structure degrades to plain size-based splitting. Both are sized in characters on purpose. A tokenizer dependency buys nothing when the embedding model has that much context headroom. *** ## Adding a format One object, no fork. ```ts theme={null} import { registerExtractor } from "@memloom/core"; registerExtractor({ kind: "html", extensions: [".html", ".htm"], version: 1, chunker: "markdown", // or "outline" async extract(bytes, path) { const text = htmlToText(new TextDecoder().decode(bytes)); return { title: findTitle(text), units: [{ text, page: null }] }; }, }); ``` Registrations are keyed by extension and the last one wins, so this also overrides a built-in. You could replace the PDF extractor with an OCR-backed one. | Rule | Why | | ---------------------------------------- | ------------------------------------------------- | | Lazy-import your parser, keep it pure JS | No native binaries, no cloud calls, light install | | One unit per page or section | Citations depend on the unit's `page` field | | Bump `version` when output changes | It salts the content hash, so files re-ingest | | Test end to end | Register, ingest, recall, assert the citation | Directories are scanned recursively. `node_modules` and dotfiles are skipped, and only files with a registered extractor are picked up. Version 1 is unsalted, so existing stores do not mass re-ingest on upgrade. # Watching files and folders Source: https://docs.memloom.dev/guides/file-sync Linked files stay current on their own, and only what changed is re-embedded ## Overview ```bash theme={null} memloom context add ~/recordings # links the folder and starts watching it memloom context roots # what is watched, and when it was last checked ``` Link a folder and memloom follows it. Edit a file and recall reflects the edit within seconds. Drop a new recording in and it gets transcribed and becomes recallable without anyone adding it. Linking is asking for something to stay current. Switch it off per folder or per file. An edited section is re-embedded. The untouched ones keep their vectors and their entity links. A file that disappears is marked, not erased. Its chunks stay recallable. *** ## What gets watched | Added as | Watched | | ------------------------------------------ | -------------------------------------- | | A folder, from the CLI, the viewer, or MCP | Yes, and files that appear in it later | | A single file linked from disk | Yes | | An upload from the browser dialog | No, there is no path to watch | | A chat attachment | No | | A web page | No, use Notion sync or re-add the page | Folders recurse five levels and skip hidden, `node_modules`, `dist`, `build`, `__pycache__` and `target`. *** ## How a change reaches recall ```mermaid theme={null} flowchart TD A[file changes on disk] --> B[watcher notices, or the rescan finds it] B --> C[ingest queue] C --> D[wait until the file stops growing] D --> E{same bytes as last time?} E -- yes --> F[nothing to do] E -- no --> G[extract and chunk] G --> H[re-embed only the chunks whose text changed] H --> I[swap chunks in one transaction] ``` Two mechanisms, because neither is enough alone. **Events** are immediate and cost nothing while idle, but they never arrive for a file that landed while the daemon was down, and network shares drop them even while it is up. **A rescan** walks every watched folder about once a minute. It is the safety net: a dropped event costs a minute, not the file. To keep it cheap it only looks at entries modified since the last pass, plus anything the store has never seen, which is how a folder copied in with its original timestamps is still noticed. Nothing is read or transcribed by the watcher itself. Every path found goes to the same ingest queue that a manual add uses, so a file still being copied is waited out, work runs one at a time, and progress and cancel work exactly as before. See [Recordings](/guides/recordings). *** ## Recording into a watched folder The case this exists for: a device that writes an audio file into a folder every minute. ```bash theme={null} memloom audio setup ``` ```bash theme={null} memloom context add ~/dropbox/recorder ``` Anything already in it is ingested, and the folder goes on the watch list. An **empty folder is fine and expected**: make the folder, link it, then point the recorder at it. Each new file is queued, transcribed, and recallable. `memloom context roots` shows the count and the last check. An hour of audio takes 8 to 11 minutes to transcribe, so a folder receiving files faster than that builds a backlog. The queue drains in order and reports it; nothing is dropped. A first link stops at 500 files, so a mistyped path cannot run away. The response says when it trimmed. Rescans of a folder already being watched are unbounded, so a folder that grows past 500 keeps working. *** ## Turning it off | | | | -------------------------------- | ----------------------------------------------------------------------------- | | One folder | `memloom context unwatch `, or **Stop watching** in the documents tab | | One file inside a watched folder | `memloom context unwatch ` | | Drop a folder from the list | `memloom context forget `, its documents stay | | The whole feature | `MEMLOOM_SYNC=off` in `~/.memloom/config.env` | Switching watching off changes nothing that was already learned, and switching it back on re-checks the folder on the next pass. | Variable | Effect | | ------------------------ | ------------------------------------------------------------------------------------------------- | | `MEMLOOM_SYNC` | `off` disables watching entirely. Folders are still recorded | | `MEMLOOM_SYNC_RESCAN_MS` | Rescan interval. Default 60000, floor 10000 | | `MEMLOOM_SYNC_POLL` | `on` forces polling instead of OS events, `off` forbids it, `auto` (default) polls only UNC paths | Polling is worth forcing when a watched folder lives on a mapped network drive, where OS events often never arrive. The rescan covers that case anyway, one interval later. *** ## Missing files A file that disappears is marked **file missing** in the documents tab. Its chunks stay recallable, because the ordinary causes are a temp-file rename, an unmounted drive, and a pipeline that tidies up after itself, and none of them mean you wanted to forget what the file said. If the file comes back, the mark clears and the file is re-read. To delete the memory as well, remove the document. ## Known limits * A file edited while the daemon is down and restored to an older timestamp is only noticed if its path is new to the store. * Renaming a file arrives as a delete plus an add, so the old document stays marked missing until you remove it. * Cloud-drive placeholder files (OneDrive on-demand, for one) are read only once the OS hydrates them. * Watching follows paths as stored. Linking the same folder twice under different letter case makes two roots. # Memory per person Source: https://docs.memloom.dev/guides/memory-per-person One store, many owners: binding an owner id so a chatbot remembers each user separately ## Two shapes of store memloom's default shape is one human and one store. Every engine method takes an optional `ownerId` and falls back to a fixed sentinel, so nothing has to be passed and nothing can be mismatched. A host app that serves many people is the other shape. A chatbot, a team server, anything where "whose memory is this" has more than one answer. Same store, same schema, an owner id per user. | Shape | Who calls | Owner | | -------------------------- | ----------------------------------- | ------------------------ | | **Single owner** (default) | CLI, daemon, your own scripts | Sentinel, implicit | | **Many owners** | Your app, keyed by its auth user id | One uuid per user, bound | *** ## Bind the owner, do not pass it Because `ownerId` defaults, a forgotten argument in a multi-tenant host is not a type error and not a runtime error. It reads and writes the shared sentinel account, so one user's memories surface in another user's session, silently. Every call still compiles. Every test still passes. So a host does not call the engine directly. It calls `forOwner()` once per request and works through the handle, where the owner is already bound and there is no argument left to forget. ```ts theme={null} import { Memloom } from "@memloom/core"; const memloom = new Memloom({ storage, embedding, llm }); await memloom.init(); // Once per request, from your own session or JWT. const mem = memloom.forOwner(session.userId); await mem.save({ content: "prefers short answers" }); const hits = await mem.recall("how do they like their answers?"); ``` `forOwner()` requires a uuid, and rejects the sentinel owner specifically. In a host the sentinel is never a real tenant. It is what an unset user id looks like, arriving as everyone's shared account, so it fails loudly instead: ```ts theme={null} memloom.forOwner(undefined); // throws: needs a uuid owner id memloom.forOwner("user_42"); // throws: needs a uuid owner id memloom.forOwner(SENTINEL_OWNER); // throws: that is what a missing user id looks like ``` If your auth system issues something other than uuids, map it once at the edge (a v5 uuid derived from your user id works well) and keep the mapping. Do not reach past the handle. *** ## What the handle covers A deliberate subset of the engine: the per-user operations a host app runs on behalf of one person. | Area | Methods | | --------- | ---------------------------------------------------------------------------- | | Memories | `save`, `recall`, `memories`, `update`, `history`, `passage`, `deleteMemory` | | Conflicts | `conflicts`, `entityConflicts`, `resolveConflict` | | Entities | `graph`, `listEntities`, `relatedEntities` | | Context | `contextAdd`, `contextList`, `contextRemove` | | Indexing | `index` | | Account | `erase` | Store-wide machinery stays on the engine: migrations, reconcile settings, embedding fingerprints, and the import and sync connectors. Those belong to whoever operates the store, not to any one tenant. `resolveConflict()` is the one place the handle does real work rather than forwarding. The engine method reads the owner off the conflict row and acts on whoever that turns out to be, which is correct for a single-owner store and a cross-tenant write for anybody else. The handle checks ownership first, and answers an id belonging to someone else exactly as it answers an id that does not exist. Which of the two it is, is itself another tenant's business. *** ## Deleting an account `erase()` removes one tenant completely: every row that owner has in every table, in one transaction. Where `deleteMemory()` removes one belief, this removes the person. ```ts theme={null} const { rows, tables } = await memloom.forOwner(userId).erase(); // { rows: 412, tables: { context_chunks: 380, memory_objects: 31, ... } } ``` The table list is discovered from the catalog rather than written down, so a table added later is covered the day it exists. Per-table counts are diagnostics, not a contract: cascades mean a child row can already be gone by the time its own table's delete runs. `erase()` covers Postgres rows only. Uploads, recordings, and transcripts that the daemon wrote to disk are the host's to remove, because core does not know where they live. If your app accepts file uploads, delete them in the same request. *** ## Sizing the connection pool `PgAdapter.connect()` defaults to a pool of 10, which suits a daemon that owns its database. A serverless host is the opposite shape: every warm container holds its own pool against one shared connection limit, so a handful of containers at 10 each exhausts it. ```ts theme={null} // One or two per container, and let the platform's pooler multiplex. const storage = await PgAdapter.connect(process.env.DATABASE_URL, { max: 2 }); ``` Point it at your platform's transaction pooler rather than the direct database port. *** ## The daemon, MCP and the CLI The daemon carries an owner too, so an agent and a person on one machine need not share memories. One client is bound to one owner, sent as the `X-Memloom-Owner` header on every request, and every owner-scoped route reads it. Reads as well as writes: writing per user and reading shared is worse than not separating at all, because it looks separated. Surfaces take the owner from their environment, never from a flag or a tool argument. Which person a surface acts for is a property of how it was launched, not of what it was asked to do, and a model calling a tool must never be the thing that chooses whose memories to read. ```bash theme={null} export MEMLOOM_OWNER_ID=11111111-1111-4111-8111-111111111111 memloom recall "how do they like their answers?" ``` For MCP, put it in the registration block your client already has: ```json theme={null} { "command": "node", "args": ["/dist/bin.js"], "env": { "OPENROUTER_API_KEY": "sk-or-...", "MEMLOOM_OWNER_ID": "11111111-1111-4111-8111-111111111111" } } ``` Leave it unset and everything shares the daemon's default owner, exactly as it always has. A malformed value is refused rather than ignored, at both ends: falling back to the shared owner because of a typo is how one person's memories reach everybody's session. `MEMLOOM_OWNER_ID` is deliberately not a `config.env` setting. That file configures the daemon, which serves every owner; this names which owner one client process is, so it belongs to that process's environment. ### The Console follows the daemon The Console is a browser page served by the daemon, so it cannot send a header of its own. It gets the daemon's default owner, which the daemon reads from `MEMLOOM_OWNER_ID` in its own environment: the shell that ran `memloom serve`, or the CLI or MCP process that auto-started it. Export the variable in your shell profile and everything agrees. That default is fixed when the daemon starts. If you set or change `MEMLOOM_OWNER_ID` while a daemon is already running, restart it: ```bash theme={null} memloom stop # the next command starts it again with the new environment ``` `GET /health` reports the default owner, so "which memories is the Console showing" is a question with an answer rather than a guess from an empty list. The daemon is a separation mechanism, not an authentication one. It binds loopback and asks for no credentials, so any local process can name any owner in that header. It keeps two agents or two people on one machine out of each other's memories. It is not a boundary between untrusted users: that tier embeds `@memloom/core` and calls `forOwner()` in its own process, where the owner comes from your session and never from the caller. Background work follows the owner too. The reconcile scheduler runs its passes for every owner in the store rather than only the default one, and the file watcher runs one instance per owner, against that owner's linked folders. A store whose memories are saved per person but maintained for only one of them looks fine for weeks and then does not. *** ## Isolation, and what is actually guaranteed Owner scoping is enforced in SQL, on every read and every write, not in a wrapper you can step around. What that buys, stated plainly: * Recall, listing, and the graph never cross owners. The sentinel owner is a third tenant, not a superuser view. * Byte-identical content saved by two owners produces two memories. The content-hash short-circuit and the dedup candidate query are both owner-scoped, so one user's belief is never merged into another's. * Reading, versioning, or deleting a memory through the wrong owner fails with the same error the id would raise if it did not exist. There is no existence leak. * Resolving a conflict names the owner, so an id belonging to someone else is refused instead of acted on. Without that argument the engine reads the owner off the conflict row and acts on whoever that turns out to be, which is right for one person and a cross-tenant write for anybody else. These are covered by `owner-isolation.test.ts`, which runs against both PGLite and real Postgres, and by `owner-http.test.ts`, which runs the same questions through the daemon. What it does not buy: memloom has no notion of who your users are. Authentication, authorization, and deciding which uuid belongs to the request in front of you are yours. *** ## The rule behind all of it: the two mistakes cost differently Deciding which owner a request belongs to means you can be wrong two ways, and they are not symmetric. **Failing to unify** is the loud, cheap one. A returning person is treated as new, so they restate what they already told you and conclude the memory feature does not work. Annoying, recoverable, and visible immediately. **Wrongly merging** is the expensive one. Two people collapse onto one owner and one person's private context surfaces inside another person's session. Preferences are the harmless end of that. The other end is a health detail, a salary, or a relationship the graph inferred from one person's memories and then served to someone else. That is not a papercut, it is a leak. So the bar for treating two things as the same owner is high, and ambiguity holds rather than merges. This is the reason behind decisions that otherwise look unhelpful: * A conflict belonging to another owner answers exactly as one that does not exist. Telling the caller which of the two it is would confirm the existence of somebody else's data. * A malformed owner id is refused rather than coerced to the default, at both the client and the daemon. Falling back to a shared owner because of a typo is a merge nobody chose. * `forOwner()` rejects the sentinel. In a host it is never a real person; it is what an unset user id looks like, and accepting it merges every caller who forgot one. If you build identity resolution above memloom, keep this asymmetry. Let deterministic signals (a verified email, an authenticated SSO subject) link on their own, make weak ones (a shared device, a name) accumulate toward a threshold, and hold whatever lands in between for review rather than merging it. An eager resolver optimises the cheap failure and walks into the expensive one. # Recordings: audio and video Source: https://docs.memloom.dev/guides/recordings Local transcription, speaker identification, and the ingest queue ## Overview ```bash theme={null} memloom audio setup # one-time: fetch the speech and speaker models, about 500 MB memloom context add "standup 2026-07-30.mkv" ``` Add a recording like any other file, from the CLI, the viewer's **Link file** button, or the MCP `add_file` tool. No audio, video or transcript leaves your machine. Recall says `from standup.mkv > 12:30 - 14:28, Speaker 2`. Name a voice once and later recordings of the same person arrive pre-named. *** ## The pipeline ```mermaid theme={null} flowchart TD A[audio or video file] --> B[hash file bytes] B --> C{transcript cached?} C -- hit --> K[rebuild sections] C -- miss --> D[ffmpeg: 16 kHz mono, every track mixed in] D --> E[VAD: find the speech] E --> F[ASR: words with timestamps] F --> G[diarization: who spoke when] G --> H[sections break on speaker turns] H --> I[voice library: match named voiceprints] I --> K K --> L[chunk, embed, store] ``` Transcription runs at roughly a tenth of real time on a recent CPU, diarization at roughly half. Both run in a worker thread, so the daemon keeps answering while an hour-long video processes. *** ## The queue Recordings never transcribe inside a request. Linking one file or fifty enqueues them. | | | | ----------------- | ---------------------------------------------------------------- | | One at a time | A recognizer holds about 1 GB | | Per-file progress | With cancel and resume | | Survives restarts | The queue is in the store | | Uploads are kept | In memloom's own directory, so playback and samples keep working | Files over about 512 MB must be linked rather than uploaded. Linking is better for any large recording, since a linked file can be re-scanned from disk later. *** ## Speakers With the speaker models installed, recordings are diarized: sections break where the speaker changes and every heading carries the label, one voice or ten. A section breaks on a change of speaker only once the current speaker has said enough to be worth finding on its own. A back-channel ("Yeah", "Oh") rides along with the speech around it, which is how a person reads a transcript. Without that floor each one became its own chunk, and since a chunk's text opens with its own heading, two words of speech sat under twenty characters of `25:46 - 25:47, Alice`. Those chunks embed as mostly the speaker's NAME, so they beat the passage that actually answers a question about that person. The documents tab shows every recording's speakers with a playable sample and inline rename. Renaming rewrites only headings and breadcrumbs, never the spoken words. **The voice library.** Each diarized voice gets a numeric voiceprint on the document, with no audio stored. Name a voice once and every later recording is compared against your named prints, renaming a confident match automatically. Each confirmation adds another print, which makes the next match better. Voices with under ten seconds of talk are never auto-named, because a short sample's print is noise. A wrong auto-name is fixed by renaming, and the manual name always wins. *** ## Models and tuning ```bash theme={null} memloom audio models # the catalog memloom audio use # switch ``` Parakeet v3 is the default, covering 25 European languages, with smaller and Asian-language alternatives. Transcripts are cached per model, so switching back costs nothing. | Variable | Effect | | ------------------------------- | ------------------------------------------------------------------------- | | `MEMLOOM_VAD_THRESHOLD` | How sure the detector must be that a frame is speech, 0 to 1. Default 0.5 | | `MEMLOOM_DIARIZE_SPEAKERS` | Pin the speaker count when you know it | | `MEMLOOM_DIARIZE_THRESHOLD` | Clustering cutoff. Smaller finds more speakers. Default 0.6 | | `MEMLOOM_VOICE_MATCH_THRESHOLD` | Auto-naming similarity bar. Default 0.8 | | `MEMLOOM_ASR_MODEL` | Pin a speech model for one run | Words and diarization are cached under separate keys, both by content hash. Changing a diarization setting reprocesses only the minutes-long diarization against the cached words, never the much longer transcription. ## Switching language mid-recording The speech model picks a language per decode call, and a call holding two of them commits to the first. Say a sentence in English and the next in Russian inside the same minute and the Russian came back empty, with no error: the words simply stopped at the switch. A chunk whose transcript ends while its speech carries on is now repacked into smaller pieces and decoded again, which is enough for each piece to hold one language. Measured on a one-minute recording that switched half way: 0 Cyrillic characters before, 203 after, with the English unchanged. Only the chunk that needs it pays for the second pass, so a single-language recording costs nothing. This is also why the check does not compare a chunk against its neighbours: a one-minute recording is a single chunk, which is exactly what an always-on wearable writes, and there is nothing to compare it with. *** ## When there is background noise Voice detection runs first, to skip silence so an hour with ten minutes of talking decodes ten minutes rather than sixty. It is an optimization, not a gate, so being wrong about where speech is never costs the recording: Speech under continuous loud audio never reaches the default threshold, because the detector sees a busy frame and the voice is a small part of its energy. Finding nothing triggers one more pass at 0.3. It costs another 0.6% of real time. Still nothing, and the file is not silent? Everything goes to the recognizer, which is a 600M-parameter speech model against the detector's 632 KB and a better judge of what it is hearing. Skipped above ten minutes, since by this point the odds are poor and an hour of music would cost 8 to 11 minutes of CPU to return nothing. A silent file, one too long for the fallback, or one the recognizer turns into no words at all, is refused with the reason. Storing a document whose only chunk is "Recorded on ..." would be worse than saying so. Background music does not by itself defeat transcription. Mixing music under a voice note at 0.7 of the speech level still transcribed completely, with a few words wrong. What fails is speech buried far below the background, where the features stop resembling speech at all. Pulling a voice out from under louder music needs a source-separation model, which is a different thing from a speech recognizer and not something memloom carries. A denoiser is not a substitute: sherpa's GTCRN enhancement made the same clip WORSE in testing, cutting the transcript short and inventing words, because it is trained on speech plus steady noise and music is neither. ## Known limits * Very short clips with seconds per voice can merge two speakers. * Overlapping speech is attributed to one speaker. * A section that absorbed a back-channel contains a word or two from another voice. It is labeled with whoever said most of it, which is the honest attribution but not a perfect one. * ffmpeg must be on PATH. * Corrupted stretches are skipped by the decoder and can degrade diarization around them. # Self-hosting and migration Source: https://docs.memloom.dev/guides/self-hosting From a folder on your laptop to a Postgres server in Docker ## The two tiers Storage is an adapter, and the same schema and SQL run on both. | Tier | Storage | Select with | | ---------------------- | ------------------------------------- | ---------------- | | **Embedded** (default) | PGLite, a folder at `~/.memloom/data` | Nothing | | **Server** | Real Postgres with pgvector, pooled | `MEMLOOM_PG_URL` | Embedded is the right default: nothing to run, and backup is copying a folder. Move to server when you want concurrent database access, a store too big for one folder, or one shared across machines. *** ## Backing up the embedded store ```bash theme={null} memloom stop cp -r ~/.memloom/data ~/backups/memloom-2026-07-12 ``` Stop first so the directory is quiescent. Restoring is the reverse. `config.env` travels separately. *** ## Running on Docker Postgres ```yaml theme={null} services: db: image: pgvector/pgvector:pg17 environment: POSTGRES_PASSWORD: memloom ports: - "5432:5432" volumes: - memloom-data:/var/lib/postgresql/data volumes: memloom-data: ``` One line in `~/.memloom/config.env`: ```bash theme={null} MEMLOOM_PG_URL=postgresql://postgres:memloom@localhost:5432/postgres ``` `memloom serve`. The first `init()` creates the whole schema on an empty database, so there is no provisioning step. The CLI, MCP and viewer all follow the same config. Two differences from embedded: the data-directory lock is unused, since Postgres handles concurrency, and the daemon does not start the wire bridge. Inspect the server directly. ```ts theme={null} import { Memloom, PgAdapter } from "@memloom/core"; const storage = await PgAdapter.connect( "postgresql://postgres:memloom@localhost:5432/postgres", ); const memloom = new Memloom({ storage, embedding, llm }); await memloom.init(); ``` `pg` is an optional dependency, so run `pnpm add pg` in the project that uses this. *** ## Migrating existing data **Re-ingest** is the simple path and loses history. Point the new store at the same embedding config and replay your sources. For a store that is mostly ingested documents this is usually enough, since documents are mirrors and re-ingesting reproduces them exactly. **Copy the rows** keeps everything. With the daemon running, pull through the wire and load into the target: ```bash theme={null} pg_dump -h 127.0.0.1 -p 54329 -U postgres -d postgres --data-only > memloom-dump.sql psql -h 127.0.0.1 -p 5432 -U postgres -d postgres -f memloom-dump.sql ``` Create the target schema first by starting `memloom serve` once with `MEMLOOM_PG_URL` set, then load data-only. The bridge is a single-connection path into PGLite. Fall back to per-table `COPY ... TO STDOUT` through `psql`, in dependency order: `_memloom_meta`, `memory_objects`, `memory_schema`, `memory_entities`, `memory_edges`, `memory_dedup_decisions`, `context_documents`, `context_chunks`, `memory_index_runs`, `memory_index_events`, `assistant_sessions`, `assistant_messages`. Skipping the later tables silently drops your schema vocabulary, indexing history and assistant chats. The embedding fingerprint travels with the data. The engine you run against a migrated store must use the same embedding provider, model and dims, or `init()` refuses to open it. See [Configuration](/guides/configuration). *** ## What stays local Whichever tier you run, the HTTP API binds to `127.0.0.1`, CORS admits only localhost origins, and the only outbound traffic goes to the provider you configured. Self-hosting the database in Docker does not expose memloom to the network unless you publish the ports yourself. # Introduction Source: https://docs.memloom.dev/index A memory engine you own, running on your machine ## Overview One Postgres-native store on your machine, shared by every client: the CLI, MCP agents, a browser viewer, and plain HTTP. Requires **Node 20 or later**. ```bash theme={null} npm install -g memloom ``` Or run everything through `npx memloom@latest `. ```bash theme={null} memloom init ``` Creates `~/.memloom` with the store and `config.env`, then starts the daemon. ```bash theme={null} memloom save "the staging database runs on Postgres" memloom recall "staging database" ``` If `memloom` is not found after installing, your Node is probably too old. npm skips a package whose `engines` it cannot satisfy, so nothing lands on your PATH. Check `node -v`, upgrade, and install again. Prefer to delegate? An agent can run this whole setup from one pasted prompt. See [Agent setup](/guides/agent-setup). *** ## Turning on cloud mode Without a key memloom runs offline with deterministic embeddings and no dedup. Set `OPENROUTER_API_KEY` in `~/.memloom/config.env`, then: ```bash theme={null} memloom stop # the store must be closed memloom reembed # recompute vectors with the new provider memloom serve ``` Offline and cloud embeddings live in different vector spaces. Mixing them makes similarity meaningless, so the daemon refuses to start until the store matches the config. That is what `reembed` is for. *** ## Memory types Every memory carries a type, and it means the same thing whichever client wrote it. Defaults to `fact`. | Type | Holds | Example | | ------------ | ------------------------------ | --------------------------------------- | | `fact` | A stable truth | *the staging database runs on Postgres* | | `preference` | How you like things done | *prefers pnpm over npm* | | `episode` | A time-bound event or decision | *shipped the viewer on 2026-07-05* | | `procedure` | Reusable steps | *to release: bump VERSION, tag, push* | Chunks from ingested files are not saved memories. They carry the sentinel type `context`. *** ## Bring your files ```bash theme={null} memloom context add ./docs memloom recall "how do we restart the ingest worker" ``` Markdown, text and PDF, chunked along real structure and fused into the same recall as your memories. Every result says where it came from: `from lecture-notes.pdf › LIMITS OF FUNCTIONS > 2. DEFINITION (p. 1)`. See [Extractors and chunking](/guides/extractors). *** ## The daemon `memloom serve` owns the store. Everything else is a client, and any CLI command starts it if it is not running. | | | | ------------- | ------------------------------------------------ | | HTTP API | `http://127.0.0.1:4319` | | Postgres wire | `postgresql://postgres@127.0.0.1:54329/postgres` | | Data | `~/.memloom/data` | | Config | `~/.memloom/config.env` | **The Postgres wire pauses the API.** PGLite takes one connection at a time, so while Drizzle Studio or psql is attached it holds an exclusive lock. Memory endpoints answer **503** instead of hanging. Disconnect the client to resume. *** ## Where next What memloom does, one card per capability. One daemon, one store, many clients. Every command. Give your agent the store. # Claude Code Source: https://docs.memloom.dev/mcp/clients/claude-code Connect Claude Code to memloom One command: ```bash theme={null} claude mcp add memloom -- npx -y @memloom/mcp ``` By default this adds memloom for the current project. Add `--scope user` to make it available in every project. Or edit the config directly, in `.mcp.json` at the project root (shared with your team) or `~/.claude.json` (just you): ```json theme={null} { "mcpServers": { "memloom": { "command": "npx", "args": ["-y", "@memloom/mcp"] } } } ``` Check the connection with `/mcp` inside Claude Code: memloom should show as connected, with its tools listed. Then follow [Verify it works](/mcp/setup#verify-it-works). # Claude Desktop Source: https://docs.memloom.dev/mcp/clients/claude-desktop Connect Claude Desktop to memloom Open Settings, then Developer, then Edit Config. That opens `claude_desktop_config.json`: * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` * Windows: `%APPDATA%\Claude\claude_desktop_config.json` Add memloom under `mcpServers`: ```json theme={null} { "mcpServers": { "memloom": { "command": "npx", "args": ["-y", "@memloom/mcp"] } } } ``` Restart Claude Desktop. The tools icon in the chat input should list memloom's tools. Then follow [Verify it works](/mcp/setup#verify-it-works). # Cline Source: https://docs.memloom.dev/mcp/clients/cline Connect Cline (VS Code extension) to memloom Click the MCP Servers icon in Cline's top bar, open the Installed tab, and press Configure MCP Servers. That opens `cline_mcp_settings.json`: ```json theme={null} { "mcpServers": { "memloom": { "command": "npx", "args": ["-y", "@memloom/mcp"] } } } ``` Save the file; Cline reloads servers automatically and shows memloom with a green indicator in the Installed tab. Then follow [Verify it works](/mcp/setup#verify-it-works). # Codex Source: https://docs.memloom.dev/mcp/clients/codex Connect the Codex CLI to memloom One command: ```bash theme={null} codex mcp add memloom -- npx -y @memloom/mcp ``` Or edit `~/.codex/config.toml` directly: ```toml theme={null} [mcp_servers.memloom] command = "npx" args = ["-y", "@memloom/mcp"] ``` `codex mcp list` shows the registered servers. Then follow [Verify it works](/mcp/setup#verify-it-works). # Cursor Source: https://docs.memloom.dev/mcp/clients/cursor Connect Cursor to memloom Create or edit `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` to enable memloom in every project): ```json theme={null} { "mcpServers": { "memloom": { "command": "npx", "args": ["-y", "@memloom/mcp"] } } } ``` You can also add it from the UI: Cursor Settings, then MCP, then Add new MCP server. Cursor shows memloom under Settings, MCP, with a green dot when the server is running. The agent uses the tools automatically; then follow [Verify it works](/mcp/setup#verify-it-works). # opencode Source: https://docs.memloom.dev/mcp/clients/opencode Connect opencode to memloom Add memloom to `opencode.json` in your project (or `~/.config/opencode/opencode.json` for all projects). Note opencode's shape: `type` is `"local"` and `command` is an array containing the command and its args together: ```json theme={null} { "$schema": "https://opencode.ai/config.json", "mcp": { "memloom": { "type": "local", "command": ["npx", "-y", "@memloom/mcp"], "enabled": true } } } ``` Restart opencode; the tools appear in the session. Then follow [Verify it works](/mcp/setup#verify-it-works). # VS Code Source: https://docs.memloom.dev/mcp/clients/vs-code Connect VS Code (Copilot agent mode) to memloom Create or edit `.vscode/mcp.json` in your workspace, or run **MCP: Open User Configuration** from the command palette for a global setup. Note the key is `servers`, not `mcpServers`: ```json theme={null} { "servers": { "memloom": { "type": "stdio", "command": "npx", "args": ["-y", "@memloom/mcp"] } } } ``` Or use the guided flow: command palette, **MCP: Add Server**, pick stdio, and enter `npx -y @memloom/mcp`. MCP tools are used by Copilot's agent mode; open the Chat view and switch to Agent. Then follow [Verify it works](/mcp/setup#verify-it-works). # Windsurf Source: https://docs.memloom.dev/mcp/clients/windsurf Connect Windsurf to memloom Open Windsurf Settings, then Cascade, then MCP servers, and view the raw config. That opens `~/.codeium/windsurf/mcp_config.json`: ```json theme={null} { "mcpServers": { "memloom": { "command": "npx", "args": ["-y", "@memloom/mcp"] } } } ``` Press the refresh button in the MCP panel (or restart Windsurf) to pick up the change. Cascade then has memloom's tools; follow [Verify it works](/mcp/setup#verify-it-works). # Zed Source: https://docs.memloom.dev/mcp/clients/zed Connect Zed to memloom Add memloom to your `settings.json` (command palette: `zed: open settings`) under `context_servers`. The `"source": "custom"` field is required; without it Zed skips the entry: ```json theme={null} { "context_servers": { "memloom": { "source": "custom", "command": "npx", "args": ["-y", "@memloom/mcp"], "env": {} } } } ``` Check the Agent Panel's settings view: the indicator dot next to memloom turns green when the server is active. Then follow [Verify it works](/mcp/setup#verify-it-works). # Setup Source: https://docs.memloom.dev/mcp/setup Connect any MCP client to your memloom store ## Overview memloom ships an MCP server over stdio. Every client comes down to the same command: ```bash theme={null} npx -y @memloom/mcp ``` It routes through the daemon rather than opening the store itself, so agents, the CLI and the viewer share one store with no lock conflicts. Run several clients at once and a memory saved from one is instantly recallable from the others. No separate install needed, `npx` fetches it on first launch. If you prefer, `npm install -g @memloom/mcp` puts `memloom-mcp` on your PATH to use as the command with no args. *** ## Your client Any other MCP client works: point it at `npx -y @memloom/mcp` as a stdio server. *** ## Where the API key goes Put `OPENROUTER_API_KEY` in `~/.memloom/config.env`, not in the client config. The daemon owns every provider call; the MCP server never talks to OpenRouter itself. An `env` block in the client config only matters when the MCP process is the one that auto-starts the daemon, because real environment variables win over the file. Keeping the key in `config.env` gives every surface the same behaviour. Restart the daemon after changing it. *** ## Verify Ask your client: > Save a memory: the staging database runs on Postgres. > What do you know about the staging database? You should see `save_memory` and `recall_memory` tool calls, and the memory appears in `memloom ui` alongside everything saved from the CLI. Full tool list on [Tools](/mcp/tools). # Tools Source: https://docs.memloom.dev/mcp/tools The fourteen tools memloom exposes to MCP clients, and how they chain ## Overview Fourteen tools in four groups. The descriptions the agent sees are written so it knows when to reach for each; this page is the human version, with the flows spelled out. `save_memory`, `recall_memory`, `read_passage`, `memory_history` `add_link`, `add_file`, `list_documents` `list_conflicts`, `resolve_conflict`, `list_possible_contradictions`, `answer_possible_contradiction` `related_entities`, `set_schema_entry_status`, `delete_schema_entry` *** ## Memories ### save\_memory | Parameter | Type | Notes | | ----------- | ------ | ------------------------------------------------------ | | `content` | string | required | | `canonical` | string | Short title, becomes the recall heading | | `type` | enum | `fact` (default), `preference`, `episode`, `procedure` | Dedupes automatically and flags contradictions instead of overwriting. The response says which happened: saved, merged, or saved with a conflict id. ### recall\_memory | Parameter | Type | Notes | | --------- | ------ | ---------- | | `query` | string | required | | `limit` | number | default 10 | Searches memories and documents together by meaning, exact words and entities. Exact identifiers like file paths and error codes make excellent queries. Each result carries a title, the passage, a similarity, provenance for chunks, and an `id`. Only active memories come back. Long passages truncate at 8k with a marker naming the escape hatch: ``` ... [truncated: call read_passage with id for the full text] ``` ### read\_passage `id` from a recall result. Exists for one flow: a result ended with the truncation marker and the answer may be in the cut part. A markdown chunk can be a whole 16k heading section, so the tail is always reachable. ### memory\_history `memoryId` from a recall result. The full version chain, newest first. Read-only. Editing a memory is a human action in the viewer or CLI. *** ## Source material Documents are citable source text, not memories. All three are versioned by content hash, so re-adding something unchanged is a cheap no-op rather than a duplicate. ### add\_link `url`. Saves a web page permanently as a document, returned by recall with the URL and section it came from. The daemon fetches and parses it, so nothing reaches a third party. A page that only renders in a browser, or sits behind a login, answers with the reason instead of a document. ### add\_file `path`, absolute, on the machine running memloom. Markdown, text, PDF, audio and video. Recordings are transcribed locally with timestamps, so a recalled line cites the moment it was said. A linked file is then [watched](/guides/file-sync): edit it on disk and recall follows, with no second call. Recordings over 10 minutes are refused with a message to relay, because transcribing an hour takes 8 to 11 minutes and no tool call should block that long. Whole folders are a CLI job: `memloom context add `. ### list\_documents `filter` matches a substring of title or path, `limit` defaults to 50. Use it to answer "is this already in?" and to see what source material recall can draw on. *** ## Decisions ### list\_conflicts, resolve\_conflict `list_conflicts` takes no parameters and shows each pending contradiction with its id. | Parameter | Type | Notes | | ------------- | ------ | ------------------------------------------------- | | `conflictId` | string | required | | `action` | enum | `keep_new`, `keep_existing`, `keep_both`, `merge` | | `candidateId` | string | required for `keep_existing` | | `content` | string | the reconciled text, required for `merge` | The agent relays the user's choice. It should not decide on its own. Every resolution is reversible. **The flow:** `save_memory` reports a contradiction with an id, `list_conflicts` shows what clashes, the user picks, `resolve_conflict` applies it. ### list\_possible\_contradictions, answer\_possible\_contradiction Unconfirmed pairs the reconcile pass flagged. Each shows the id, a verbatim quote from both sides, the reason, and the similarity. | Parameter | Type | Notes | | --------- | ------ | ---------------------- | | `id` | string | from the list | | `answer` | enum | `confirm` or `dismiss` | These are not conflicts. The pass runs at roughly 40 percent precision, so most of what it returns is wrong. `confirm` creates a real conflict; `dismiss` is permanent with no undo. Ask the user rather than guessing: a wrong confirm makes noise they can clear, a wrong dismiss loses a real contradiction for good. See [reconciliation](/concepts/reconciliation) for what the pass costs. *** ## Graph vocabulary ### related\_entities | Parameter | Type | Notes | | --------- | ------ | --------------------------------------------- | | `entity` | string | a name, an id, or a known alias | | `type` | string | optional filter, one of the nine entity types | | `limit` | number | default 50 | For questions about connections rather than content, where recall would return prose the agent still has to read. The target is matched exactly, so this is a graph lookup, not a second search. A folded-away spelling resolves to its canonical and the answer says so: asking about `Bob` answers about `Robert` and reports they are the same. Two groups come back separately: **stated relationships** the extractor asserted, and **appears together with**, entities named by the same sources. An agent relaying the answer must not present the second as the first. One was asserted, the other merely observed. ### set\_schema\_entry\_status, delete\_schema\_entry | Parameter | Type | Notes | | --------- | ------ | ------------------------------------------ | | `kind` | enum | `entity_type` or `predicate` | | `name` | string | case-insensitive | | `status` | enum | `active` or `disabled`, for the first tool | Disabling stops future extraction using an entry; entities already extracted under it stay. Deleting works only on a **disabled** entry you added: built-ins can only be disabled, and an active entry must be disabled first. Refusals come back as an explanation, not an error. # Use cases Source: https://docs.memloom.dev/use-cases What you can do with a local memory engine ## Memory for coding agents Register memloom as an MCP server ([setup](/mcp/setup)) and the agent gets `save_memory` and `recall_memory` backed by your store. "deploys go through `scripts/release.sh`, never manual" saved once, recalled in every later session. One daemon, one store. What Claude Code saved is there in Cursor or Codex. No vendor owns it. Dedup absorbs the noise, and contradiction detection stops a confused agent overwriting a good fact. *** ## A second brain your agents share ```bash theme={null} memloom context add ./notes ``` If you know NotebookLM, this is that idea, but local, open source, and readable by every agent you connect. | | | | ------------------------ | ------------------------------------------------------------------------- | | **Recallable both ways** | By meaning and by exact phrase, cited back to file, section and page | | **One copy** | Every tool reads the same store instead of keeping its own partial one | | **Stays in step** | Re-run `context add`: changed files re-ingest, unchanged ones are skipped |