Matthew Smith201 downloadsGive Claude Code safe, reviewable memory: the agent proposes memories to a review inbox and nothing is written without your approval. Local-first Markdown, no cloud key required, hardened localhost server.
Safe, reviewable memory for Claude Code — stored as plain Markdown in your own vault.
Coder Engram is the safe memory layer for Claude Code. It turns your Obsidian vault into persistent, structured memory an AI coding agent can search and propose to — but every agent-written memory lands in a review inbox you approve or discard, so nothing is ever silently written to or edited in your notes. Everything is plain Markdown inside a Claude Code/ folder in your vault; the retrieval index is a rebuildable local cache. No cloud API key is required for the default experience, and the optional local server is off by default, binds 127.0.0.1, and is token-authenticated.
Most Obsidian ↔ AI plugins are either a chat panel or a bridge that hands an agent broad read/write/edit access to your notes. Engram is neither:
Status: 0.15.1. No index rebuild — upgrading changes nothing about your data. (0.15.0 rebuilt the index once, a local re-chunk with no re-embed, so a widened tag exclusion reached notes already indexed; if you are coming from 0.14.x that rebuild still happens.) Your memory under
Claude Code/Memory/is untouched;Index/is a rebuildable cache. The local server is disabled by default and binds to127.0.0.1. Vector retrieval is disabled by default too — the embedding provider defaults tonone, so search stays fully offline and lexical until you point it at a local Ollama or an OpenAI-compatible endpoint. Attachment indexing is likewise opt-in, and local for every format except image text — that one delegates OCR to the Text Extractor plugin, which fetches its language data on first use (see Network use). See CHANGELOG.md for release history and docs/ROADMAP.md for what is still deferred.
Claude Code/.pending-memory.md) by default, so nothing overwrites your notes without review.pending-memory.md as a reviewable card showing its resolved destination, with per-entry Apply, Edit & apply, and Discard. Applying appends into the destination memory file and removes the entry from the inbox. Promotion is desktop-UI-only — never exposed over the network — always append-only, and validated inside the memory root.Claude Code/: project overview, architecture, decisions, tasks, and open questions, plus timestamped session notes and global profile / preferences / conventions.Quartzine Protocol.md is findable by its name and an alias-only hub note is reachable at all.npm run eval scores golden queries (recall@8 / MRR per query class) and the context cost of an answer; npm run bench measures index build and query latency at production scale.get_note_context returns one note's indexed text passage by passage with headings and line ranges — the follow-up to a search hit. Supports ranged reads (startLine / endLine) to jump straight into a long note, and an outline mode that returns a headings-only map (line range + breadcrumb, no body) as a cheap survey before a full read. Truncated reads name the exact line to continue from.find_related_notes walks the wikilink graph from an indexed note — what it links to and what links back.summarize_note. Returns a selection of the note's own sentences, verbatim and in original order — never generated prose, because there is no LLM backend. Ranked by lexical frequency-centrality offline, or by embedding-centroid similarity with MMR when a provider is reachable.get_recent_changes answers "what moved since I last looked" from the note→mtime map the index already holds — no query, no scoring, no I/O. Returns paths and dates rather than content, so the agent picks what to read next. An empty index is reported distinctly from "nothing changed": the answer is unknown, not negative.search_batch runs several related queries in one call and returns one de-duplicated page, each hit naming which queries it answered. A chunk several queries agree on ranks above one only a single query found — a signal you lose asking them separately.resolve_project maps a working directory or repo name to the project name this vault actually uses, so a near miss stops returning empty context that reads as "nothing here yet".list_pending_memory shows the proposals the agent has made that are still awaiting your review, so it stops re-proposing what is already queued. Read-only — approving or discarding stays in Obsidian, where a person does it.resolveInVault surfaces where it is defined rather than whichever note mentions it most. find_symbol looks a definition up by exact name. Upgrading rebuilds the index once — memory is untouched, Index/ is a cache.tokenBudget, so an agent can bound a call in the unit its context window is actually measured in. Estimated and deliberately conservative: you get a little less than you asked for, never more, and results are cut at whole-result boundaries.add_memory tells the agent when a proposal covers ground you already have. Every proposal is scored against your existing memory, and the closest match comes back with it — so a fact that has changed gets re-proposed as a replacement instead of piling up beside the old one. It reports the overlap; it never suppresses the proposal.add_memory can propose a replacement. Passing supersedes (a "<path>#<heading>" from a search result) marks the memory it makes stale. When you apply the entry, that memory stops coming back from search and from context reads — its text stays in the file, and a record in Memory/Inbox/superseded-memory.md says what retired it. Delete the record to bring it back. Only memory files can be retired, only a named section of one, and only when you apply the entry yourself.list_rejected_memory shows the proposals you discarded, and the reason you gave. Discarding records the entry in Memory/Inbox/rejected-memory.md; an identical proposal is then refused with that reason attached, so the agent stops re-proposing what you have already turned down. Also read-only — clearing the ledger (which lets those memories be proposed again) is a button in the review modal.With Index attachments on, text-bearing attachments are extracted and indexed exactly like notes — same chunking, same incremental refresh, same exclusions, same tools:
Page N section per page).docx, pptx, xlsx — and LibreOffice — odt, odp, ods.txt, csv), and Canvas boards (text cards, group labels, edge labels).Extraction for all of the above is dependency-free and fully local: the bytes never leave your machine, extracted text is cached in a rebuildable index file, and turning the setting off deletes that cache. (Images are the one exception, and are a separate opt-in — see below.)
Text inside images is a separate opt-in (Index text inside images). Rather than bundling an OCR engine — megabytes of WebAssembly, plus language data fetched at runtime, which Obsidian's developer policy disallows — it delegates to the Text Extractor plugin if you have it. With that plugin absent, nothing happens. Note that Text Extractor downloads its OCR language data on first use, so this is the one attachment path that can touch the network. Scanned PDFs still yield no text.
127.0.0.1, constant-time bearer-token auth with a failed-auth lockout, DNS-rebinding (Host/Origin) guards, POST-only with a 1 MB body cap, socket timeouts, and per-tool rate limits.The server is a thin node:http shell (src/server/local-server.ts) around pure, unit-tested MCP layers. It is off by default. To use it:
http://127.0.0.1:3999 (default port) with the token as a bearer credential.Writes proposed over the network always go to the review inbox — the server never performs direct writes, even if Allow direct memory writes is enabled in the desktop settings. See docs/MCP_SERVER.md and docs/CLAUDE_CODE_INTEGRATION.md.
Vector search is opt-in. The Embedding provider setting defaults to none, and until you change it retrieval is lexical BM25 only — no network calls, no API key. Two providers are available:
http://127.0.0.1:11434). No API key, and note text never leaves your machine. Set the Embedding model to a model your Ollama has pulled./embeddings endpoint (OpenAI, LM Studio, LocalAI, vLLM, …). This sends your indexed note text to the configured endpoint, which may be remote — an explicit, opt-in data-egress choice. It requires an endpoint, a model, and an API key (a secret, stored locally and never logged). The settings UI shows a notice when you select it.Embedding happens at index time (reindex/refresh) and is cached in Index/embeddings.json inside the vault; unchanged chunks are reused so re-embedding is incremental. Retrieval mode (lexical, hybrid, or vector; default hybrid) controls how vectors are used. If the provider is unavailable — unset, unreachable, or erroring — search transparently degrades to lexical rather than failing. Excluded/sensitive notes are never indexed, so they are never embedded or sent anywhere. See docs/SECURITY.md for the data-egress details.
Download main.js, manifest.json, and styles.css from a release.
Optionally verify them. Every release also publishes SHA256SUMS, and from v0.9.0 the assets carry signed build provenance, so you can confirm they were built by the release workflow from the tag they claim:
sha256sum -c SHA256SUMS --ignore-missing # or: shasum -a 256 -c
gh attestation verify main.js --repo nfoav8or/coder-engram
Create the folder <vault>/.obsidian/plugins/coder-engram/.
Copy the three files into that folder.
In Obsidian: Settings → Community plugins → Reload plugins, then enable Coder Engram.
This is a desktop-only plugin (isDesktopOnly: true, minimum Obsidian 1.13.0).
scripts/install.sh automates the steps above: it downloads the release assets, verifies them against the release's SHA256SUMS, copies them into a vault you pick (auto-detected from Obsidian's vault registry), and — only with --enable — turns the plugin on. Verification fails closed: if the checksum manifest cannot be fetched, or no sha256 tool is available, it refuses to install rather than continuing unverified — whoever can tamper with a download can also block one request, so an absent manifest is treated as interference rather than assumed benign. Releases before v0.6.0 predate the manifest; --skip-verify is the explicit opt-out for those. In keeping with this project's safety posture, please download and read it rather than piping it straight to bash:
curl -fsSL https://raw.githubusercontent.com/nfoav8or/coder-engram/main/scripts/install.sh -o install.sh
less install.sh # read what you're about to run
bash install.sh --vault "/path/to/YourVault"
Run bash install.sh --help for options (--version x.y.z, --enable). Requires curl plus python3 or jq for vault auto-detection. Checksum verification uses whichever of sha256sum, shasum, or openssl the machine has — stock macOS ships no sha256sum — and refuses to install on a mismatch, or on an asset the manifest does not cover.
Build the plugin (see Building).
Copy the build outputs — main.js, manifest.json, and styles.css — into:
<vault>/.obsidian/plugins/coder-engram/
Reload plugins in Obsidian and enable Coder Engram.
npm install
npm run dev # esbuild watch build
npm run dev rebuilds main.js on change. Point it at a test vault by copying (or symlinking) the outputs into that vault's .obsidian/plugins/coder-engram/ folder. See docs/DEVELOPMENT.md for details.
npm run build # typecheck + production esbuild bundle
Other scripts:
| Script | Purpose |
|---|---|
npm run dev |
Watch build (development) |
npm run build |
Typecheck then production bundle |
npm run test |
Run the Vitest suite once |
npm run test:watch |
Vitest in watch mode |
npm run lint |
ESLint over .ts sources |
npm run typecheck |
tsc --noEmit |
npm run test:e2e |
Drives the built plugin in a real Obsidian. Needs a display — it skips with exit 0 when DISPLAY is unset, so check its output rather than its exit code. |
npm run bench |
Index build and query latency at production scale |
npm run eval |
Relevance scoring (recall@8 / MRR per query class) over golden queries |
Releases are published by the release.yml GitHub Actions workflow whenever a
version tag is pushed. It runs the full gate (typecheck, lint, test, build),
attests the built artifacts, and attaches main.js, manifest.json,
styles.css, and a SHA256SUMS manifest to a GitHub release.
These four files are always updated as part of cutting a release, not only when the change happened to touch them. They are the ones that go stale silently, because nothing fails when they do:
| File | What to check |
|---|---|
CHANGELOG.md |
The Unreleased section becomes the new version with a date, and the link references at the bottom gain a row for it. |
README.md |
The Status: line names the new version, and the Roadmap section's last bullet describes it. Anything the release changed about commands, settings, scripts, or defaults is reflected in the tables above. |
docs/ROADMAP.md |
A row in the patch-release table, and "In progress (unreleased)" names the version just cut. |
| Subsystem docs | docs/SECURITY.md, docs/RAG_PIPELINE.md, docs/ARCHITECTURE.md, docs/MCP_SERVER.md, docs/MEMORY_MODEL.md, docs/LARGE_VAULTS.md — whichever the change touched. |
The README is on that list because it is the only one a reader sees first and the only one no other document links to for its facts, so nothing else going stale reveals it.
npm version 0.10.3 # bumps package.json + syncs manifest.json / versions.json
git push origin develop
git push origin 0.10.3 # the bare tag; this is what triggers release.yml
Two checks run before anything is published: the tag must equal the
manifest.json version exactly — bare, with no v prefix, because Obsidian
resolves the download from a release tagged identically to that version — and versions.json must
carry an entry for it that agrees with the manifest's minAppVersion — which is
what npm version writes, so a mismatch means the manifest was edited by hand.
main is not updated by the workflow. Fast-forward it so the release tag is in
its history — otherwise the tags accumulate on develop only, and main keeps
claiming an older version:
git push origin develop:main
Then verify the published bytes rather than assuming them: sha256sum -c SHA256SUMS
against the downloaded assets, and gh attestation verify main.js --repo <owner>/coder-engram
(exit 0 is the signal — it can print nothing on success).
Settings live under Settings → Coder Engram. Key settings and their safe defaults:
| Setting | Default | Notes |
|---|---|---|
| Enable indexing | true |
Scan and index vault notes. |
| Memory root | Claude Code |
Vault-relative folder for all plugin-managed memory. Must stay inside the vault. |
| Included folders | (empty) | Allowlist; empty means the whole vault. |
| Excluded folders | (empty) | Folders to skip. Matched on whole path segments, ignoring case and Unicode form (so an accented name typed here matches the same name stored decomposed by macOS). |
| Excluded tags | (empty) | Notes with any of these tags are never indexed. Matched ignoring case and Unicode form, in frontmatter and inline #tags, in any script. Excluding a tag also excludes its children — private covers #private/secret — as Obsidian's own tag search does. |
| Excluded path patterns | (empty) | Glob (* within a segment, ** across zero or more directories) or substring patterns for sensitive notes. A leading / is accepted and means the vault root. |
| Index attachments | false |
Extract and index text from PDFs (Obsidian's bundled PDF engine), Office documents (docx/pptx/xlsx, odt/odp/ods, rtf — dependency-free extraction), plain text (txt/csv), and Canvas cards, locally. Exclusions apply; extracted text is searchable/readable over the local server like any note. |
| Index text inside images | false |
Reads text out of PNG/JPG/WEBP/BMP attachments by delegating to the Text Extractor plugin; with that plugin absent, nothing happens. The one attachment path that can cause network activity — Text Extractor downloads its OCR language data on first use. Requires Index attachments. |
| Auto-index on file change | false |
Debounced (~2.5s) refresh when notes change. |
| Default project | (empty) | Used by project-context and add-to-project commands. |
| Embedding provider | none |
Lexical BM25 always works with none. ollama and openai-compatible enable vector/hybrid retrieval; mock is a deterministic dev provider. |
| Embedding model | (empty) | Model name for the selected provider (required for ollama and openai-compatible). |
| Retrieval mode | hybrid |
lexical, hybrid, or vector. Forced to lexical whenever the provider is none or unavailable. The control panel shows the mode actually in effect, and says so when a configured provider has no vectors yet. |
| Memory ageing (half-life in days) | 0 (off) |
Ranks older memory lower, halving its weight every N days. Ordinary notes are untouched, and the weight is floored so an old memory drops down the list but never becomes unfindable. Clamped to 0–3650. |
| Embedding endpoint | (empty) | Base URL for the provider. Ollama defaults to http://127.0.0.1:11434; OpenAI-compatible needs the full base URL including any version prefix. |
| Embedding API key | (empty) | Secret bearer key for OpenAI-compatible endpoints. Stored locally, sent only in the Authorization header, and never logged. |
| Embedding batch size | 16 |
Chunks embedded per request at index time; clamped to 1–512. |
| Concurrent batches | 1 |
Embedding batches in flight at once; clamped to 1–8. Keep at 1 for hosted APIs; 2–4 speeds up the first pass against a local Ollama. |
| Enable local server | false |
Disabled by default. Localhost MCP/HTTP bridge for Claude Code. |
| Server host | 127.0.0.1 |
Localhost only unless "Allow non-localhost binding" is on. |
| Server port | 3999 |
|
| Server token | (empty) | Bearer token for server requests; compared in constant time. Required (16+ characters) to bind a non-localhost host. |
| Allow non-localhost binding | false |
Off by default. Binding a non-localhost host also requires a 16+ character token. Exposes memory to your network — not recommended. |
| Allow direct memory writes | false |
When off, all writes go to the review inbox. |
| Append-only writes | true |
Writes only append, never overwrite. |
| Collapse near-duplicate hits | false |
Drops a search hit whose text nearly repeats a higher-ranked one. |
| Cap one note's share of a page | false |
Stops one long note filling a whole result page. |
| Merge overlapping passages | false |
Joins a section's consecutive windows on a full-note read, sending the carried overlap once. |
| Debug logging | false |
Logs to the developer console; secrets are always redacted. |
The memory root is validated on entry: a value that would escape the vault is rejected.
The plugin registers twelve commands. Obsidian prefixes each with the plugin name and shows them in sentence case, so search the palette for "Coder Engram":
The Control Panel (right sidebar, also on the ribbon "brain-circuit" icon) shows the memory root, indexed-note and chunk counts, last-indexed time, and server status, plus quick buttons: Reindex, Search, Add memory, Review inbox, New project, Project context.
Programmatic access from Claude Code runs over the local MCP/HTTP server, which is disabled by default. Once you set a token and enable it, Claude Code can search memory, propose entries (inbox-first), and read project/global context. You can still use the plugin entirely through Obsidian commands and by reading/writing the Markdown memory folder yourself.
See docs/MCP_SERVER.md for the server design and docs/CLAUDE_CODE_INTEGRATION.md for the workflow and an MCP config example.
All plugin-managed memory lives under the configured root (default Claude Code/):
Claude Code/
Memory/
Global/
profile.md
preferences.md
conventions.md
Projects/
<project-name>/
overview.md
architecture.md
decisions.md
tasks.md
open-questions.md
sessions/
YYYY-MM-DD-HHMM.md
Inbox/
pending-memory.md
Index/
chunks.json
metadata.json
embeddings.json
Config/
plugin-settings-backup.json
Markdown files are the durable source of truth; the JSON files under Index/ are a rebuildable cache. See docs/MEMORY_MODEL.md.
resolveInVault is the single path choke-point and rejects absolute paths and .. traversal.127.0.0.1, uses constant-time bearer-token auth, and applies DNS-rebinding (Host/Origin) guards. It refuses to bind a non-localhost host unless you both allow it and set a token. Server writes are always inbox-first, and no generic file access or full-vault dump is exposed.Full details: docs/SECURITY.md.
By default the plugin makes no network connections at all: search is offline lexical BM25, and the local server is off. Network activity exists only when you explicitly enable it:
Embeddings (opt-in). If you set the embedding provider to Ollama, the plugin sends indexed note text to your configured Ollama endpoint (local by default, http://127.0.0.1:11434) to compute embeddings. If you set it to OpenAI-compatible, indexed note text is sent to the endpoint you configure (which may be a remote service such as OpenAI) for the same purpose; the API key is sent only in the Authorization header and never logged. Notes excluded from indexing are never embedded, so their content is never sent anywhere.
Local MCP/HTTP server (opt-in). When enabled, the plugin listens on 127.0.0.1 so local tools such as Claude Code can query memory and propose entries to the review inbox. It makes no outbound connections; binding a non-localhost address requires an explicit second opt-in plus a token.
Text inside images (opt-in). This path delegates OCR to the Text Extractor plugin, which downloads its language data from the internet on first use. That download is the companion plugin's behaviour, not this one's — your image bytes are not sent anywhere — but enabling Index text inside images is what triggers it, so it is listed here rather than buried in the feature description.
Every other attachment path — PDF, Office, RTF, plain text, Canvas — runs entirely locally, and the bytes never leave your machine. There is no telemetry of any kind, and nothing is read or written outside the vault.
summarize_note is extractive, not abstractive. It selects the note's own sentences; there is no LLM/generative backend, so it never rewrites or paraphrases. It also only works on notes that are in the index.isDesktopOnly: true.summarize_note (Summarize Current Note command + MCP tool).version-bump.mjs tooling that keeps manifest.json/versions.json in sync.get_note_context, find_related_notes, add_memory de-duplication, embeddings no-op-persist guard.[PENDING REVIEW] labels, bounded and rate-limited context tools, O(changed) refresh I/O, and the embedding-settings reload fix.setHeading()/setTitle(), CSS-class styling, network-use disclosure), and a settings/UI correctness bundle (blur-commit settings, server config snapshot + no-op rebind skip, reindex guard, search-race fix).Index/ and the review inbox — is now treated as something that may have changed in between, because sync, another tool, or the plugin racing itself can change it. And the plugin stopped assuming your notes are in English: search tokenized non-Latin text into nothing, an excluded inline tag like #privé was misread so that privacy filter was failing open, and (0.10.5) summarize_note had kept its own copy of the ASCII-only pattern, scoring every non-Latin sentence 0. 0.10.5 and 0.10.6 also stop re-doing whole-vault work — the embedding pass, scan-rule folding, the find_related_notes link graph, vector-norm computation, per-chunk content hashing, and per-note chunk lookup now skip or cache what an unchanged index already proved. 0.10.6 additionally fixes two review-loop bugs (a corrupt settings blob could crash server startup; a pending memory whose content ends in a "Related files:"-shaped list was mis-parsed into metadata) and pins the CI/release workflows' actions to commit SHAs. 0.10.7 starts the large-vault track: the vector cache moves to a compact binary format (migrated in place, no re-embed), lexical search gains an inverted index, and the embedding pass becomes resumable via checkpoints with optional concurrent batches for local providers.(, so **#private**, urgent,#private,todo and "#private" all extracted nothing, and an unterminated frontmatter block discarded its tags: list entirely. Both are fixed and the index rebuilds once to evict notes that should never have been in it. Also: the last chunk of a note ending in a newline no longer reports a line past the end, add_memory stopped re-parsing the whole review inbox on every call, and the two source findings from Obsidian's automated 0.11.0 review are closed.tags: list, and an inline tag was missed after most punctuation — so notes marked to stay away from the agent were indexed anyway. Two more fix costs 0.11.1 itself introduced: an index-version bump forced a full paid re-embed of the vault, and hybrid search then silently degraded to lexical while still reporting "hybrid". Also fixed: a settings change mid-pass could discard an entire reindex, a blank Excluded-folders entry disabled indexing outright, and the inbox dedup cache could drop a genuinely new memory. The plugin also stopped claiming an embedding update it had not performed, and scripts/install.sh now refuses to install unverified. Upgrading rebuilds the index once — re-chunking only, no re-embedding.tags: block list dropped every tag after it, which affects ordinary terminated frontmatter and predates 0.11.2. Upgrading rebuilds the index once — re-chunking only.Host hostname against the bound host without requiring either to be non-empty, and a whitespace-only configured host survived its 127.0.0.1 fallback to bind every interface — the exact exposure the non-localhost opt-in exists to gate. Neither was practically attackable (both need that opt-in, which also forces a token), but a guard reading two empty strings as agreement is the wrong shape. Retrieval and summarization now hold their own vector invariants instead of trusting the caller: a stored cosine norm is recomputed rather than read back on faith, where a desynced one would have inflated a score past 1 and outranked every honest match. Property fuzzing closed four contract gaps — one reachable, where add_memory with a blank related path wrote a malformed bullet the parser then silently dropped. Both remaining findings from Obsidian's review scan are closed at the class level: every rethrow now throws a provable Error, and all nine timer sites schedule through a host-aware helper. No index rebuild — upgrading changes nothing about your data.versions.json still maps every release up to 0.11.4 to 1.7.2, so an app below 1.13 is offered 0.11.4 and keeps a fully working plugin, it just stops getting new features. On 1.13 or later you need do nothing. Raising the floor let the imperative settings tab go — about 450 lines that duplicated every settings row, and which Obsidian has ignored since 1.13 anyway. Diffing the two paths before deleting either turned up a real gap: the sentence stating that nothing is written outside the vault existed only in the imperative tab, so 1.13+ users had already stopped seeing it; it is restored. No index rebuild.Private/**/*.md into the literal Private/.md — a string essentially no path contains — so the exclusion matched nothing and the notes it was meant to hide were indexed and reachable over the local server, with nothing logged. The fallback now requires the pattern's literal fragments to appear in order, which errs toward excluding more rather than less, and says so in the log. If you use a path pattern with many wildcards, check the notes it covers after upgrading. No index rebuild — the first refresh drops anything that should not have been indexed.reindex_vault
consults its cooldown before it refuses, so a refusal is never free to flood.
Block-reference wikilinks ([[Note^id]]) resolve in the link graph, a
4-backtick fence is no longer closed by a 3-backtick line inside it, UTF-16
text attachments decode instead of indexing as mojibake, a zip whose length
fields run past the archive is refused rather than read as empty, and
using namespace std; no longer declares std. Lexical scoring walks each
query term's posting list rather than testing every term against every
candidate: byte-identical scores (pinned), 26–40% faster. No index rebuild./ in a path pattern matching nothing, ** refusing to match zero
directories, a wrapped tags: list losing every tag after the first line,
and a tag exclusion not covering its children (now it does, as Obsidian's own
tag search does; rebuilds the index once to reach notes already indexed).
A discarded proposal no longer comes back through search as unlabelled memory.
Two ways an append could overwrite are closed, and a file that an interrupted
write left parked under a backup name is restored at the next load — proven on
real Obsidian by the e2e run. Error messages stop disclosing the vault's
absolute path after a separator the old check did not name; the rebinding
guard checks the Host header against the bound address in every mode; project
names are made safe at the boundary that promises it. Word's punctuation in
RTF stopped decoding to invisible characters that broke the words around them,
and one unreadable slide no longer discards a whole deck. Summarization is
6× faster with identical output, and a sharded embeddings cache loads in one
concurrent pass. The control panel shows the retrieval mode actually serving
results.# is one. An
ordinary ## line inside an applied memory ended a retired section early, so
superseding reported success while still serving the memory it had retired,
and a supersedes path that was not already canonical recorded a retirement
that could never match. And tokenBudget could be overrun nearly sixfold by a
single result, because nothing bounded the heading every result label embeds.
Also: a truncation notice was added on top of a cap instead of counted against
it, a truncated answer could end in half an emoji, and get_note_context told
an agent to reindex a note that was indexed and deliberately retired. No
index rebuild — but a note already in the heading state above is re-chunked
only when you next edit it.list_rejected_memory closes the loop list_pending_memory opened, and an identical proposal is refused with that reason instead of coming back every session. A proposal can name the memory it replaces; applying it retires the old one from search, context reads, note reads and symbol lookup without ever overwriting the text, and deleting one ledger record brings it back. Every proposal is scored against existing memory so a fact that changed gets re-proposed as a replacement rather than piling up beside the old one. Memory ageing (opt-in, floored, memory-only) stops an eighteen-month-old decision outranking last week's. Tools answer with structured results alongside their prose and accept a tokenBudget, so a caller can cite a passage by field and bound a call in the unit its context window is measured in. And chunks record the symbols their code declares, so asking for an identifier finds where it is defined — find_symbol looks one up by name. Rebuilds the index once on upgrade.list_pending_memory lets it see its own proposals — add_memory only ever reported that something landed, so an agent could not tell an accepted memory from a rejected one from a still-pending one, and re-proposed facts it had already contributed. get_recent_changes answers "what moved since I last looked" from the index's own note→mtime map, with no query, no scoring and no I/O. resolve_project maps a working directory or repo name to the project name the vault actually uses, so a near miss stops returning empty context that reads as "nothing here yet" — the same fix now applies to the Default project setting in Obsidian, which had the identical bug. search_batch runs several related queries in one call and merges them by rank, so a chunk several queries agree on outranks one only a single query found. No index rebuild.Details: docs/ROADMAP.md.
See CONTRIBUTING.md for the gate to run, the two layering rules the build enforces, and how to check that a new test actually holds something.
MIT. See LICENSE.