Natthapol Maneechote587 downloadsLocal-LLM (Ollama) chat orchestrator with topic-based memory, adjustable retrieval, and hierarchical document summarization.
Give your local AI persistent, scalable memory through hierarchical context retrieval.
The Librarium is a local-LLM chat orchestrator for Obsidian, backed by Ollama. It keeps topic-separated memory as a fixed stack of progressively more detailed layers — from a quick Overview down to a Comprehensive Summary, plus the raw Original — routes each chat query to only the relevant topics, and searches that layer stack starting from the least detail, loading more only when it's actually needed.
Everything runs locally against your own Ollama models: no cloud API, no data leaving your machine — with one opt-out exception, basic internet crawling (off with one toggle if you want a fully offline setup; see Grep search & basic web crawling).


The Librarium is an Obsidian plugin, so you need Obsidian installed first:
The Librarium doesn't call any model itself — it talks to a local Ollama server for chat, summarization, and embeddings.
Download and install Ollama from ollama.com (macOS, Windows, and Linux are all supported).
Confirm it's running — by default it serves at http://localhost:11434.
Pull the models you plan to use, for example:
ollama pull gemma4:e4b # chat model
ollama pull nomic-embed-text # embedding model
The chat and summary models must support chat/generate. The embedding model must be an embeddings-only model (e.g. nomic-embed-text) — it cannot be used for chat, and a chat model can't be used for embeddings. Mixing these up is the most common source of 404/500 errors (see Known limitations).
Copy manifest.json, main.js, and styles.css into:
<vault>/.obsidian/plugins/the-librarium/
Then enable it from Settings → Community plugins in Obsidian, open the plugin's settings tab, point it at your Ollama models, and use the "Test connection" button to confirm all three models are reachable and correctly configured.
npm run dev runs esbuild in watch mode while you iterate on the source.
The Librarium has been primarily developed and tested using the following environment. Other configurations should work, but performance and throughput may vary depending on the selected models and hardware.
| Component | Specification |
|---|---|
| CPU | AMD Ryzen 7 7800X3D |
| GPU | NVIDIA GeForce RTX 5060 Ti 16 GB |
| RAM | 32 GB |
| Operating System | Windows 11 Pro |
| Purpose | Model |
|---|---|
| Chat | gemma4:e4b |
| Summarization | gemma4:e4b |
| Embeddings | nomic-embed-text |
The default configuration were used for development and testing:
ollamaBaseUrl: "http://localhost:11434",
chatModel: "gemma4:e4b",
summaryModel: "gemma4:e4b",
embeddingModel: "nomic-embed-text",
memoriesFolder: "librarium/brains/memories",
tempMemoryFolder: "librarium/brains/temp-memory",
noteMemoryFolder: "librarium/brains/notes",
autoInitNoteMemory: true,
maxMemoriesPerQuery: 3,
routingMethod: "hybrid",
suggestMemoryUpdates: true,
enableClarification: true,
similarityThreshold: 0.55,
enableEmbeddingCache: true,
embeddingCacheSize: 200,
maxChunkMergePasses: 4,
maxConcurrentSummaries: 4,
mergeGroupMaxChars: 20000,
mergeOverlapUnits: 1,
numAbstractionLayers: 3,
enableIntentExtraction: true,
responseLanguage: "auto",
documentsFolder: "librarium/documents",
enableFileCommands: true,
enableGrepSearch: true,
enableWebCrawling: true,
enableSkillCreation: true,
skillsFolder: "librarium/brains/skills",
skillResearchMaxPages: 4,
enableResearch: true,
researchMaxSites: 12,
researchMaxSubtopicDepth: 2,
researchMaxSubtopicFiles: 12,
enableKnowledgeBase: true,
knowledgeMaxEntitiesPerQuery: 5,
trackChatSummary: true,
recentRawTurns: 12,
debugLogging: false,
These settings are intended as a balanced default for local inference. Users with more powerful hardware may benefit from increasing maxConcurrentSummaries, while lower-memory systems may prefer reducing concurrency or selecting a smaller chat model.
The tested defaults above are a reasonable starting point for a mid-range single-GPU machine running a small-to-mid chat model. Beyond that baseline, a few settings are worth tuning deliberately depending on your hardware and how you actually use the plugin — each adds cost (an extra LLM call, a bigger context window, or more concurrent requests) in exchange for a specific benefit, so there's no single "best" value for everyone.
| Setting | Trade-off | Recommendation |
|---|---|---|
maxConcurrentSummaries |
More parallel requests to Ollama = faster builds, but higher peak VRAM/CPU load | Keep the default (4) on the tested hardware class above. Drop to 1–2 on integrated GPUs or CPU-only inference; raise to 6–8 only if you have a larger/multiple GPU(s) and see Ollama comfortably keeping up. |
numAbstractionLayers |
More layers = finer-grained control over how much detail gets pulled in, but a longer build/extend chain per topic | 3 (default) is a good general middle ground. Drop to 1–2 for mostly short, simple notes where an Overview vs. Comprehensive Summary split is already enough. Raise it only for large, dense source documents where you actually want a mid-tier "just enough detail" layer. |
routingMethod |
llm is most accurate but doesn't scale; embedding scales but is a blunter match; hybrid balances both at the cost of needing a working embedding model |
Use hybrid (default) once you have more than a handful of topics. llm is fine and slightly more accurate below ~20 topics. Fall back to embedding only if your embedding model is unreliable or you want to skip the LLM re-rank call entirely. |
enableIntentExtraction / enableClarification |
Each is one extra small-model call per query, in exchange for sharper retrieval and fewer wrong-context answers | Leave both on with a fast summaryModel (they're cheap relative to the main chat call). If you're on CPU-only inference and every round trip is expensive, turn enableClarification off first — it matters most for ambiguous, personal-context questions, less for self-contained ones. |
suggestMemoryUpdates |
Surfaces candidate facts to confirm/discard — change-aware, so it only re-surfaces a topic when something's actually new or different | Leave on. It only stages something when it's judged genuinely new or changed relative to what's already remembered or already pending, so it shouldn't get noisy even in long chats. |
trackChatSummary |
One extra small-model call per turn, in exchange for the model staying aware of earlier turns without re-sending the whole transcript | Leave on for any chat you expect to run more than a few turns — it's what keeps later replies grounded once recentRawTurns starts trimming the raw transcript. Turn it off only for short, one-off Q&A sessions where the extra call isn't worth it. |
recentRawTurns |
Smaller = cheaper/faster per-turn context, larger = more exact verbatim recall of recent wording | 12 (default) suits a typical 8K–32K-context chat model. Lower it to 6–8 for smaller-context or slower models once trackChatSummary is on, since the summary picks up the slack. Raise it if your chat model has a large context window and you'd rather over-provide raw context than rely on the summary's compression. |
Three other things worth calling out explicitly:
chunkMaxChars / chunkMaxSentences). That's gone — src/chunker.ts now asks the LLM to mark natural break points (topic shifts, scene/section boundaries) instead, so there's nothing to configure here. The only ceiling left is an internal, non-configurable input-size limit that exists purely to keep a single chunking call within the model's context window.chatModel and summaryModel (as in the tested config) is simplest and works well if that model is fast enough; if it isn't, pointing summaryModel at a smaller/faster model than chatModel is usually the single highest-leverage change, since summaryModel is what pays for routing, intent extraction, ambiguity checks, memory-command detection, the chat-history digest, and every layer build/extend.maxConcurrentSummaries is the one setting most directly tied to your specific machine rather than to how you use the plugin — when in doubt, start low, watch Ollama's own load, and raise it incrementally rather than guessing from GPU memory alone.The Librarium maps onto these building blocks:
1. Layered memory — src/memoryStore.ts. Each topic is a main note (Overview + links) plus a companion folder with one file per deeper layer and the raw Original. Everything is always derived from the topic's LayeredMemory — never written by hand — whether the topic came from an ingested file or purely from chat-derived facts. The LayeredMemory itself lives in the plugin's data.json; the vault files are always regenerated from it.
2. Routing — src/memoryRouter.ts. On every chat query, routeMemories() looks at all topic overviews and picks the relevant ones, capped by the adjustable Max memories per query setting. See Routing below.
3. File reading/writing/deleting/moving — src/fileSkills.ts. A thin wrapper around the vault API (read/write/append/delete/move/create-folder/list) used by the orchestrator, note-memory, and the file-management command flow below.
4. Progressive abstraction — src/summarizer.ts. Every memory is a fixed, named stack of layers, built bottom-up so each one is a faithful, non-hallucinated abstraction of the layer below it. See Memory layers.
5. Chat history digest — src/chatHistoryStore.ts. Each chat session keeps a rolling summary of the conversation plus the model's current best read of what the user is trying to accomplish, updated with one small-model call per turn instead of re-reading the whole transcript. See Chat history digest below.
6. Document commands — src/fileCommands.ts. Natural-language requests — in any language, phrased any number of ways, not just a fixed set of English keywords — to write, update, delete, move, or reorganize documents/notes/folders are turned into a concrete plan. Explicit requests ("write this into a file", "create a document for this") are carried out immediately and reported in chat; ambiguous ones, and anything destructive (delete/move), are staged as a confirm-before-applying plan instead. See Document commands below.
7. Grep search & basic web crawling — src/grepSearch.ts and src/webFetch.ts. Keyword/quoted-phrase search across vault notes, and fetching URLs or running a lightweight web search, both folded in as extra context when relevant. See Grep search & basic web crawling below.
8. Skills — src/skillStore.ts and src/skillCommands.ts. Reusable how-to notes built from real fetched web pages, created via an explicit command (natural language or a direct /skill <url> command) or an opportunistic, confirm-first suggestion. See Skills below.
9. Research — src/researchCommands.ts. Explicit multi-site research (natural language or a direct /research <topic> command) that groups sites by topic, asks the user to disambiguate genuinely different topics found along the way, and writes findings up as document(s). See Research below.
10. Knowledge base — src/knowledgeBase.ts, src/knowledgeIndex.ts, src/knowledgeExtraction.ts, src/knowledgeTypes.ts. Entities/facts/relationships/evidence extracted and accumulated from every document/research write, resolved incrementally against existing entities, and retrieved via a bounded, indexed lookup rather than a full scan — so query time stays roughly flat as the KB grows. See Knowledge base below.
Librarium keeps two separate top-level vault folders, both under librarium/ by default (each piece changeable in settings):
librarium/brains/ — Librarium's own internal space: memory, context, and every other system-managed store. Not meant to be hand-edited.
librarium/brains/memories/ — confirmed, permanent memory-topic files: a main note per topic plus a same-named companion folder.librarium/brains/temp-memory/ — pending, unconfirmed candidates, shown as Save/Discard cards in chat before anything is written to permanent memory.librarium/brains/notes/ — per-note layered mirrors (note-memory).librarium/brains/skills/ — saved skill notes, built from web research.librarium/documents/ — the user-facing space. Anything created or edited via a document command (see above) lives here, organized into topic subfolders as it's created; Librarium prefers updating an existing document over creating a near-duplicate one whenever a good match exists.
The chat-history digest is the one exception to "everything lives in the vault": it's pure scratch context for the model, never shown to the user and never written as a note, so it lives only in the plugin's data.json alongside the LayeredMemory data.
| Layer | Name | Contains |
|---|---|---|
| 0 (top) | Overview | a few sentences giving a quick understanding |
| 1 | High-Level Concepts | main ideas, themes, and relationships |
| 2 | Detailed Concepts | specific explanations, important details, supporting context |
| 3 (base) | Comprehensive Summary | near-complete, preserves most of the original information |
| — | Original | the complete, unmodified source text |
The number of layers is configurable (numAbstractionLayers, default 3). Build order runs bottom-up: the Comprehensive Summary is built directly from the source (chunked, then read carefully and merged if it doesn't fit in one chunk); every layer above it is produced by compressing the layer directly below, and is explicitly forbidden from introducing information that isn't already there. At query time, resolution starts at the Overview and only descends into more detail if the current layer is judged insufficient, falling through to the raw Original only as an absolute last resort.
When a topic grows (a new fact from chat, or another file merged in), extendLayeredMemory() builds a summary of just the new text, merges it into the existing Comprehensive Summary, and recascades every layer above it — so growth cost stays proportional to the (much smaller) Comprehensive Summary rather than the full raw history.
Before creating a new topic, the orchestrator also checks (via a cheap embedding pass, with an LLM tiebreaker for borderline matches) whether the new content actually belongs under an existing topic, so near-duplicate topics don't fragment your memory.
A query is first routed to a shortlist of topics (routeMemories()), using one of three strategies (set in the settings tab):
llm — shows the model every topic overview and asks it to pick ids. Most flexible; costs one generation call; doesn't scale well past ~50–100 topics.embedding — cosine similarity between the query and each topic's overview embedding. Scales to many topics, cheap, no LLM round trip.hybrid (default) — embeddings shortlist ~2x the cap, then the LLM re-ranks/filters that shortlist. Good balance once your topic count grows.Every topic-overview embedding computed for embedding/hybrid routing (and for the file-ingestion topic-match check) is kept in an in-memory LRU cache for the session, so a topic already embedded once isn't re-sent to Ollama on the next query — controlled by Cache topic embeddings (enableEmbeddingCache, on by default) and Embedding cache size (embeddingCacheSize, default 200 vectors, oldest evicted first once full). Turning the toggle off makes every lookup hit Ollama fresh, same as before this cache existed.
Resolving how much detail to pull from the routed topics is then done as one joint, layer-by-layer search across all of them together (resolveAcrossSources() in src/hierarchicalQuery.ts), not by resolving each topic independently:
irrelevant (dropped, deeper layers never fetched), sufficient (this cheap layer is kept as-is), or descend (relevant, but needs the next, more detailed layer of that topic).descend continue into the next layer, together, in the same kind of batched call — down to the Comprehensive Summary and, only if still insufficient, the raw Original.This means total LLM calls scale with layer depth (numAbstractionLayers), not with how many topics were routed — irrelevant topics never cost more than the price of their Overview.
The currently open note (when "Include current note" is on, or the query says "this note"/"the current note") is not folded into the batched search above — it's resolved on its own via resolveFromLayers(). This is deliberate: the user explicitly asked for that note, so it shouldn't be silently dropped by a relevance verdict the way a routed topic can be.
resolveFromLayers() makes one LLM call regardless of numAbstractionLayers: the model is shown every layer of the note at once (Overview through Comprehensive Summary, each with its full text) plus the query and its distilled intent, and picks the lowest-detail layer that's sufficient — or answers need_original if none of the named layers are enough, which pulls in the raw Original text. Only that one selected layer (or the Original) is then loaded into the chat context; the others were only ever shown as summaries for the model to choose from. This keeps a query that includes the current note to one extra round trip on top of intent extraction, topic routing, the topic search, and the final chat call — not one round trip per layer.
src/noteMemoryStore.ts is distinct from both permanent topics and temp-memory: a 1:1 layered mirror of a single note, stored the same way as a topic (a main mirror note plus a companion folder of layer files). The first time a note is referenced in chat, its mirror is built automatically (autoInitNoteMemory, on by default), and a query resolves against that layered memory instead of dumping the note's full raw text every time.
A mirror is never refreshed automatically after that first build — you do it explicitly: the toolbar's build/rebuild note-memory button (see Chat interface) handles whatever note is open; "Refresh (full)" / "Update (incremental)" appear under an answer that used one; the same two actions are also available from the command palette. Incremental update diffs the note against what was last synced: a clean append only summarizes and merges the new suffix; an edit in the middle falls back to a full rebuild.
Every chat session (src/chatHistoryStore.ts) keeps a SessionSummary — a compact narrative of the conversation so far, plus the model's current best read of what the user is overall trying to accomplish in that chat — updated with one merge call per turn (previous digest + this turn → new digest), the same incremental pattern extendLayeredMemory() uses for topic memories. It's pure scratch context: never shown in the chat UI, never written to the vault, and cleared automatically when a session is deleted or pruned.
This digest does two things once a session has more than a couple of turns:
detectMemoryCommand), query-intent extraction (extractQueryIntent), and the ambiguity check (checkAmbiguity) all see it, not just the last handful of raw messages, so pronoun resolution and "is this ambiguous?" judgments stay grounded even deep into a long chat.recentRawTurns messages are sent verbatim; older turns are represented only by the summary. This keeps per-turn cost roughly flat as a chat grows instead of paying for the entire transcript on every single message.Both behaviors are controlled from the settings tab: Track chat summary (on by default) toggles the whole feature, and Recent raw turns to include verbatim (default 12) controls the cap. Turning the toggle off falls back to sending the full raw transcript every time, same as before this feature existed.
The Response language setting (responseLanguage, default "auto") controls what language the model replies in, independent of what language your memory notes or vault content happen to be written in.
auto (default) — every reply matches whatever language your latest message is written in, and follows along if you switch languages mid-conversation. This is a plain instruction appended to the system prompt (src/language.ts) on every chat answer, clarifying question, and generated file-write content — it doesn't run a separate detection pass, so it's free.Spanish, 日本語, Français) — pins every reply to that language regardless of what language you write in, until you change the setting back.Session titles (generateShortTitle) also follow this setting.
Chat recognizes natural-language requests to change what's actually in your documents — not just answer questions about them — and turns them into a concrete, ordered plan (src/fileCommands.ts for detection, Orchestrator.executeFileActionPlan for applying it). Recognition is semantic, not a fixed keyword list: it works across natural phrasings and languages, not just literal English words like "file" or "document". All of these are recognized as the same underlying intent:
todo.md to archive/todo-2025.md."Detection is two-stage. A cheap, deliberately broad multilingual regex (FILE_COMMAND_HINT) first checks whether the message is even plausibly related to documents/files — it exists purely to avoid an LLM call on ordinary chat, and errs heavily toward over-matching. The actual yes/no decision, and whether the request is explicit ("create a document for this" — a direct, unambiguous command) or ambiguous (softer phrasing, uncertain intent), is made by one LLM call grounded in the vault's real file/folder listing, so it can't invent paths — it breaks the request into an ordered list of steps (write, delete, deleteFolder, move, createFolder). Before creating a brand-new document, a second small check looks for an existing document under documentsFolder that's clearly the right place for the content instead, so documents/ doesn't accumulate near-duplicates — you'll see this happen in chat (e.g. "Librarium: Found documents/game-design/combat.md. Updating it instead of creating a new document.").
Explicit requests are carried out directly. If the request was explicit and only involves writing or creating folders (never anything destructive), it's applied immediately — the user's own phrasing already counted as consent — and the result is reported as its own chat message ("Librarium: ✓ Write documents/... (1,204 chars)"), not just a flash in the status line. Anything ambiguous, or that deletes or moves something, is still rendered as a confirm card in the chat panel with "Do it" and "Cancel" buttons; nothing happens until you tap "Do it" (deletes go through Obsidian's trash, so they're recoverable either way).
New documents always live under documentsFolder (librarium/documents by default), organized into topic subfolders — kept separate from Librarium's own internal librarium/brains/ data, which document commands can never see or touch. Existing files anywhere else in the vault can still be read, updated, moved, or deleted using their real path.
Tidy up the whole documents folder on demand — ask "tidy up my documents", "organize my documents folder" (or run /tidy-documents, a direct command, no LLM call) and Librarium rereads every document currently in documentsFolder (src/documentsTidy.ts) and proposes a folder/subfolder reorganization by topic — reusing an existing subfolder where one fits, proposing a new one for a real cluster of related documents, and deliberately leaving anything that doesn't clearly belong with others exactly where it is rather than dumping it in a catch-all. This is expressed as ordinary move steps, so it goes through the exact same confirm card as any other file plan — nothing actually moves until you tap "Do it".
Toggle: Enable file management commands (enableFileCommands, on by default) — also gates the tidy-up command. Folder: Documents folder (documentsFolder, default librarium/documents).
Two independent, on-by-default extras, each with its own settings-tab toggle, both following the same "cheap regex pre-filter, only pay for the real thing if it hits" shape as memory-command detection.
Grep search (src/grepSearch.ts, enableGrepSearch) does two things:
grepVault) — matching lines, with a couple lines of surrounding context, are folded into the chat context alongside whatever routed memories were found.grepText) — genuine "read just the relevant part" instead of "read the first N characters" — falling back to the old truncated read only if grep finds nothing (e.g. "summarize this note", which has no distinctive search term of its own).Basic internet crawling (src/webFetch.ts, enableWebCrawling):
requestUrl, so no CORS issues) and stripped down to plain text as context.A skill (src/skillStore.ts, enableSkillCreation, on by default) is a saved, reusable "how-to" note, written from actual fetched web pages rather than the model's own memory — its own note under the Skills folder (skillsFolder, default librarium/brains/skills), distinct from memory topics, temp-memory, and note-memory. There are two ways one gets created, and only one of them asks first:
1. Explicit commands — researched and saved right away, no confirmation needed (src/skillCommands.ts, same "asking is consent" logic as the "remember this" memory command):
/skill <url> [optional topic], e.g. /skill https://www.postgresql.org/docs/current/warm-standby.html Postgres replication. Parsed with a plain regex, no LLM call at all, so it's instant and unambiguous — the direct way to point a skill at a specific source.Either way, if URL(s) were given (explicitly, or via /skill), those pages are fetched directly; otherwise a basic web search (the same webSearch()/fetchUrl() from basic internet crawling above) runs on the topic and the top results are fetched — capped by Max pages per skill (skillResearchMaxPages, default 4). The fetched pages are then synthesized into a practical write-up (concrete steps, key facts, gotchas — never inventing anything the sources don't support) and saved. Re-researching a topic that matches an existing skill's name updates that skill (merging in any newly-found sources) instead of creating a near-duplicate.
2. Opportunistic suggestion — always asks first, nothing researched until you say yes: after answering a normal query, one LLM call (detectSkillOpportunity, shown the names/descriptions of skills already saved, so it doesn't re-suggest something already covered) judges whether the exchange represents a durable, reusable "how do I do X" capability genuinely worth having as a skill. If so, chat shows a confirm card — "Research & save" or "No thanks" — and only tapping the former actually runs any research or writes anything.
Research (src/researchCommands.ts for detection/grouping, Orchestrator.researchTopic/writeResearchDocuments for the rest, enableResearch, on by default) is a separate feature from skills above: instead of one durable how-to guide, it searches a topic across several sites, works out whether they're really all about the same thing, and writes the findings up as document(s) in the documents folder — while pausing to ask if the sites turn out to be about genuinely different things.
Trigger it explicitly, same "asking is consent" logic as skills/memory commands:
/research <topic> or /research <url> [url...] [topic], parsed with a plain regex, no LLM call, instant and unambiguous.The procedure (Orchestrator.researchTopic):
researchMaxSites, default 12, vs. 4 for skills), since telling distinct topics apart needs enough material to actually separate into groups.groupPagesByTopic), grounded in each page's real URL/title/excerpt so it can't invent a source. Sites are only split into separate groups when they reflect genuinely different interpretations of the topic (e.g. "Apple" the technology company vs. the fruit) — not just different angles or sub-aspects of the same subject. Any site that doesn't clearly fit a real interpretation at all is dropped rather than forced into a group.planTopicStructure, src/researchPlan.ts) looks at the topic and what's already been found and decides whether it's narrow enough for a single document, or broad enough to deserve a structured breakdown — e.g. "research and construct a pandas documentation folder" produces documents/pandas/ with its own dataframes.md, series.md, io.md, etc., each individually researched with its own targeted search (not just reusing the original topic-level sources), plus a generated overview file linking them. For a genuinely broad subtopic, the model can go one level deeper still (a sub-subtopic folder) — how many layers actually get used is the model's call, not a fixed setting; Max subtopic depth (researchMaxSubtopicDepth, default 2) and Max subtopic files (researchMaxSubtopicFiles, default 12) only set the hard ceiling it has to plan within. A narrow topic ("my cat's breed") still gets exactly one flat document, same as before.documentsFolder/the planned subfolder, checking first (same matching used by document commands) whether an existing document is clearly the right place for it instead of creating a near-duplicate. Every write is reported as its own "Librarium: ..." chat message.Memory topics and research documents above are still whole-document summaries. The knowledge base (src/knowledgeTypes.ts, src/knowledgeIndex.ts, src/knowledgeExtraction.ts, src/knowledgeBase.ts; enableKnowledgeBase, on by default) sits underneath them as a second, finer-grained layer: entities, atomic facts, typed relationships, and attributes, accumulated persistently across every document ever written — turning
Documents → Embeddings → Vector Search → LLM
into
Documents
↓
Facts + Entities + Relationships + Evidence
↓
Persistent Accumulated Knowledge
↓
Indexed Retrieval
↓
Relevant Chunks
↓
LLM
The point of this layer is that query cost stays bounded as the KB grows, not that it grows more accurate — it's specifically the piece that keeps the plugin from getting slower to query the more documents it accumulates.
Rebuilding is always manual — never triggered by writing a document. Run "Knowledge base: rebuild from documents folder" from the command palette, or the /rebuild-knowledge chat command (instant, no LLM call to trigger it), and Librarium rereads every document currently under documentsFolder. A content hash is kept per document (KnowledgeBaseData.sourceHashes), so a document whose text hasn't changed since the last rebuild is skipped entirely — the very first rebuild after adding a lot of documents processes all of them, but every rebuild after that only pays for what's actually new or edited, even though it scans the whole folder each time. This is deliberate: extraction quality benefits from seeing a document's final, settled content rather than reacting to every intermediate edit, and it keeps rebuild cost something you control rather than something that happens on every keystroke-adjacent write.
Ingestion (KnowledgeBase.ingestDocument, called once per document by a rebuild):
chunkTextWithLLM, same as memory topics).extractKnowledge) that pulls out entities (each tagged with a coarse type — "technology company" vs. "fruit" — so same-named-but-different things never get conflated later), atomic single-sentence facts (each with a confidence and, where applicable, a structured attribute/value pair), and typed relationships between entities. Nothing is invented outside what the chunk actually says.KnowledgeIndex, over names/aliases/types/attributes/relationship-types) narrows the whole KB down to a small shortlist first; an exact name/alias match with a matching type merges immediately; otherwise the shortlist is ranked by embedding similarity, with a strong match auto-merging, a borderline one getting one bounded LLM disambiguation call against just that shortlist, and nothing close enough creating a new entity. This is what keeps "Apple Inc." (type: technology company) and "apple" (type: fruit) as two separate entities even though the name overlaps — their types differ, so an exact match is rejected and their embeddings score far apart.supersededBy rather than deleted — conflicting or outdated facts stay inspectable instead of being silently overwritten.entity.summary, 2–4 sentences derived from that entity's own facts) regenerated and re-embedded — an unrelated entity elsewhere in the KB is never re-derived, so rebuilding costs roughly the same per new/changed document regardless of how much knowledge already exists.Query (KnowledgeBase.query), run alongside memory-topic routing on every turn, against whatever the KB held as of the last rebuild:
knowledgeMaxEntitiesPerQuery, default 5) are kept.Because both rebuilding and query only ever touch a bounded candidate set (an index shortlist, plus the specific entities/facts that set points at, plus — for rebuilds — only documents whose hash actually changed) rather than the whole entity/fact/relationship graph, the plugin gets more knowledgeable as documents accumulate without query latency growing with total KB size — the design goal for this layer.
The chat panel (src/chatView.ts) has:
Answers render through Obsidian's own markdown renderer (headings, lists, code blocks, links), have a hover-to-reveal copy button, and the panel auto-scrolls to new messages only when you're already near the bottom.
While a chat turn is in flight, the status line under the input box updates in real time with what the orchestrator is actually doing right now, instead of sitting on one static "thinking" message for the whole round trip — e.g. "Checking for a file-management request..." → "Routing to relevant memories..." → "Searching the vault for..." → "Fetching https://..." → "Writing the answer...".
This is purely cosmetic (an optional onStage callback passed into Orchestrator.handleQuery/provideClarification/executeFileActionPlan, wired up in src/chatView.ts) — it never changes what's retrieved or how the turn is answered, it just narrates each major step as it starts, including the vault search and web-fetch steps above, and the individual steps of an applied file-action plan.
Obsidian's requestUrl (what src/ollamaClient.ts uses to talk to Ollama) has no abort/signal support, so Cancel here is cooperative, not a true abort: a CancellationToken is checked between discrete steps of a multi-step operation, and everywhere a chat turn, a memory build, or a search touches a checkpoint, it stops before starting the next step.
Concretely, clicking Cancel stops queuing new work immediately and discards whatever single call was already in flight the moment it returns, rather than showing or recording it. This feels close to instant for anything with many small steps (a Comprehensive Summary built from a dozen chunks, a multi-round memory search). Cancelling right as the final chat response is being generated is the one case where you still wait roughly as long as that call would have taken anyway, since there's nothing to interrupt it with — only somewhere to discard it once it lands.
/api/chat supports it, but responses currently only render once complete.checkAmbiguity() and extractQueryIntent() each run one small call on every query; turn either off in settings if you want fewer round trips and mostly ask self-contained questions.trackChatSummary is on (the default) — it's a merge call against the previous digest, not a re-read of the whole transcript, but it's still a round trip you didn't pay before this feature existed. The digest itself isn't shown or editable in the UI; if it ever drifts from the actual conversation, clearing/restarting the chat session is currently the only reset.extendLayeredMemory() recascades every layer above the Comprehensive Summary on each growth — cheap per call, but a frequently-extended topic still pays a full recascade every time rather than batching.ChatSessionStore caps history at 30 sessions, auto-pruning the oldest (and their temp-memory) beyond that.workspace.getActiveFile() goes null the instant focus moves into the chat panel itself (it isn't a file-view), which would otherwise silently break "reading page" mode and note-memory auto-init on almost every real turn. src/activeFileTracker.ts works around this by remembering the last note that was genuinely focused before that. Edge case: if you close that note's tab entirely (rather than just click away from it) before asking about it, there's nothing left to remember and the active-note context is skipped.maxConcurrentSummaries has no awareness of what your Ollama instance can actually handle concurrently — the default (4) is a reasonable middle ground, not a measured optimum for your hardware.requestUrl reaching the public internet and on DuckDuckGo's HTML endpoint keeping its current markup; if DuckDuckGo changes its page structure, webSearch()'s result-parsing regex may stop matching until it's updated. Turn enableWebCrawling off for a fully offline setup.detectFileCommand's vault listing (and therefore what it can propose write/delete/move steps for) is listMarkdownFiles(); non-markdown vault files (images, PDFs, etc.) aren't part of the planning context, though deleteFolder/createFolder steps still affect whatever a folder contains.FILE_COMMAND_HINT is intentionally broad (covers common phrasing in ~10 languages) so it rarely produces a false negative, but it can still miss an unusual paraphrase in a language/script it doesn't have vocabulary for; the actual explicit-vs-ambiguous and yes/no decision is always made by the LLM call behind it, not the regex itself.memoriesFolder/tempMemoryFolder/noteMemoryFolder/skillsFolder away from their pre-update defaults, nothing is moved automatically; update the folder settings by hand if you want the new layout./skill at a specific URL sidesteps that.routeMemories() only routes memory topics; a saved skill is a note you (or "this note" mode) can reference directly, but it isn't yet folded into ordinary query answering the way memory topics are./research <url> ...) sidestep that. The topic-grouping step is a single LLM call over short excerpts of each page, not the full fetched text, so a genuinely ambiguous topic with very similar-reading excerpts across interpretations could still end up in one group rather than being split out for you to choose.documentsFolder — since it's manual by design (see "Rebuilding is always manual" above), a document written or edited after the last "Knowledge base: rebuild from documents folder" run//rebuild-knowledge simply isn't in the KB yet until you run it again; a memory topic or skill note created through those separate features also isn't covered, since a rebuild only reads documentsFolder.resolveEntity/query never look beyond the token index's candidate list (CANDIDATE_LIMIT, currently 30), so a mention using vocabulary that shares no token at all with an existing entity's name/aliases/type/attributes (e.g. referring to "the Cupertino company" with no other overlapping wording) can miss a real match and create a duplicate entity rather than merging — this is the explicit trade-off that keeps both a rebuild and query cost independent of total KB size, per the design goal, rather than an oversight; a stray duplicate is still evidence-linked and inspectable in the accumulated data, not silently wrong.supersededBy the other) and both surface in an entity's profile/context rather than the plugin silently deciding which is right; that's intentional (see "Handle uncertainty/conflicts" in the KB design), but it does mean the model is the one that has to reason about the disagreement at answer time, same as it would with two conflicting documents.The Librarium is completely free and open source. If it has helped improve your workflow, consider supporting its continued development.
Your support helps maintain the project, improve features, and continue building better local-first AI tools.
MIT © natthapolmnc