repo-memory is an MCP server over stdio with a layered design. Each layer is a focused module; the persistence layer underneath is SQLite in WAL mode, stored at .repo-memory/cache.db in the target project root. Source: github.com/blamechris/repo-memory.

Layers

MCP Server (stdio transport)
├── Cache Engine (hash, store, invalidation, ranking, GC)
├── Indexer Pipeline (scanner, summarizer, imports, diff-analyzer)
├── Dependency Graph (persisted import edges in SQLite + freshness gate)
├── Task Memory (CRUD, exploration tracking, frontier)
├── Telemetry (token tracking, sampling, export, retention)
├── Session Manager (cross-turn persistence)
└── Persistence Layer (SQLite with WAL mode)
  • Cache Engine — hashes files (SHA-256), stores and invalidates summaries, ranks by access frequency, and garbage-collects stale entries. GC runs automatically on server startup, with age thresholds set in [[install-and-configuration#garbage-collection|the gc config block]].
  • Indexer Pipeline — discovers files (respecting .gitignore), summarizes them via the configured engine (tree-sitter ASTs by default, [[install-and-configuration#summarizer|"summarizer": "regex"]] to opt out), extracts imports/exports, and detects changes through a diff-analyzer.
  • Dependency Graph — import edges persisted in SQLite (the imports table), written whenever a summary is generated and served back through a freshness gate: graph queries load the stored edges, stat-check files for changes (with a 2-second safety window against coarse filesystem timestamps), and re-read only files whose hash actually changed. A warm query touches no project files (0.13.0; earlier versions re-read every file and rewrote the whole table per call). Powers [[tools-reference#get_dependency_graph|get_dependency_graph]] and [[tools-reference#get_related_files|get_related_files]]. Design notes: Dependency Graph.
  • Task Memory — investigation-task CRUD plus exploration tracking and the unexplored frontier, powering the task tools. Design notes: Task Memory.
  • Telemetry — token tracking with sampling, export, and retention, powering [[tools-reference#get_token_report|get_token_report]].
  • Session Manager — cross-turn session persistence so investigations survive context-window resets.

How the Cache Works

First access (cache miss): the agent calls [[tools-reference#get_file_summary|get_file_summary]]. The server reads the file, SHA-256 hashes it, extracts a summary via the configured summarizer (exports, imports, purpose, declarations, line count), stores the hash plus summary in SQLite, and returns the compact summary. No savings yet — the file had to be read. (The [[tools-reference#the-repo-memory-index-cli|repo-memory index CLI]] can pay this first-read cost ahead of time, e.g. as a git post-merge hook or CI step.)

Subsequent access (cache hit): the server re-reads and hashes the file; the hash matches the stored value, so it returns the cached summary without re-parsing. The savings — full-file tokens minus summary tokens — are logged.

When files change: the hash no longer matches, so a fresh summary is generated automatically. You never get stale data. The savings compound because an agent typically touches the same files 3-5 times per session: the first pass pays full price, every later hit returns a small summary.

Token Savings

Telemetry records every cache interaction so savings can be measured exactly. Token estimates use the standard heuristic of about 4 characters per token, which closely matches major tokenizers (cl100k_base, o200k_base). For each cache hit:

tokensSaved = ceil(rawFileChars / 4) - ceil(summaryJsonChars / 4)
EventWhenTokens recorded
cache_hitSummary served from cache (hash unchanged)Tokens saved (raw file minus summary)
cache_missFile changed or first access0 (no savings on first read)
force_rereadExplicit re-read requestedRaw file token count
invalidationCache entry cleared
summary_servedA search_by_purpose query returned results — one event per query (0.13.0)Estimated tokens of one average result file read in full
search_missA search_by_purpose query matched nothing against a non-empty corpus (0.17.0)0 — books no savings; carries the query in metadata

The per-query summary_served accounting replaced per-match accounting in 0.13.0: booking every matched file as a full read avoided inflated “tokens saved” by up to the result limit, when the realistic counterfactual for one search is grepping and then reading roughly one candidate. Telemetry writes on read paths are best-effort — a locked database can never fail a search.

Prewarm runs via the [[tools-reference#the-repo-memory-index-cli|repo-memory index CLI]] record no telemetry events (0.11.0+) — earlier versions logged a cache_miss per indexed file, which distorted agent-traffic hit-ratio stats. The report from [[tools-reference#get_token_report|get_token_report]] therefore reflects agent traffic only.

Performance

Benchmarks on synthetic TypeScript projects with realistic imports and class structures show a sustained ~3.6x compression ratio and sub-millisecond per-file cached reads:

ScenarioFilesRaw SizeSummary SizeCompressionTokens SavedSpeed
Explore project1011.7 KB3.3 KB3.6x~2,1003.7 ms/file
Explore project5058.0 KB16.2 KB3.6x~10,7000.7 ms/file
Explore project100116.1 KB32.3 KB3.6x~21,5000.4 ms/file
Explore project200233.4 KB65.7 KB3.6x~42,9000.3 ms/file

Run them yourself with npm run benchmark.

Language Support

Summaries are extracted from tree-sitter parse trees by default (0.12.0+), or via regex analysis when [[install-and-configuration#summarizer|"summarizer": "regex"]] is set. All six language families below have AST support, which adds semantic purpose lines derived from doc comments; regex stays as the universal fallback for other languages and unparseable files.

  • TypeScript / JavaScript — exports, imports, declarations, purpose classification; AST mode adds JSDoc-derived purpose lines
  • Python — functions, classes (incl. async def), __all__, from/import statements; AST mode adds docstring-derived purpose lines
  • Go — exported names (uppercase), imports, type/func/var/const declarations; AST mode adds doc-comment purpose lines and grouped var (…) / const (…) support
  • Rustpub items, use/mod statements, structs/enums/traits/impls; AST mode adds /// doc-comment purpose lines and pub use re-exports
  • Kotlin (.kt/.kts, 0.11.0+) — AST mode only: public top-level fun/class/object/interface/enum class/data class/val/var/typealias (excluding private/internal), import paths, KDoc-derived purpose lines; regex mode gives only basic filename classification
  • Java (0.11.0+) — AST mode only: public types and the public methods/fields of the public type, import statements (incl. static and wildcard), Javadoc-derived purpose lines; regex mode gives only basic filename classification

The dependency graph ([[tools-reference#get_related_files|get_related_files]], [[tools-reference#get_dependency_graph|get_dependency_graph]]) extracts imports for all six language families regardless of summarizer mode — both tools share one extension list (0.13.0; they previously disagreed, so Kotlin/Java repos silently got empty graphs from one of them). Relative import targets are resolved to real on-disk paths at extraction time (./store.jssrc/cache/store.ts), and bare specifiers (packages, builtins) are tagged external and kept out of the persisted graph, so every path the graph returns can actually be read.

Config files (JSON, YAML, TOML) and other types get basic classification.

Design Decisions

  • SQLite with WAL mode — concurrent reads while writing; database at .repo-memory/cache.db in the target project.
  • SHA-256 hashing — deterministic file comparison; unchanged hash means the cached summary is still valid.
  • POSIX-normalized paths — all stored paths (cache keys, import edges, task files) use forward slashes regardless of platform, so lookups and prefix scoping (e.g. search_by_purpose’s pathPrefix) behave identically on Windows and Unix.
  • Per-key config validation — an invalid value in .repo-memory.json is skipped with a warning while the valid keys still apply; only an unreadable or unparseable file falls back fully to defaults.
  • AST-first summarization, regex fallback — the default engine (0.12.0+) is tree-sitter (pure WASM, no native compilation) for exact exports and semantic purpose lines, with per-file regex fallback on parse errors and for languages without a grammar; "summarizer": "regex" opts out entirely. The AST spike exposed the regex engine’s biggest accuracy hole — export async function declarations were invisible to its export pattern (15 of 35 files in repo-memory’s own src/) — which has since been fixed in the regex engine too. See the AST summarizer design notes.
  • Monotonic generation tags — the cache records which summarizer generation produced its summaries. A process at an older package version than the tag serves read-through without clearing, re-tagging, or persisting its own summaries, so a long-running server and a newer npx hook sharing one cache can never thrash it (0.13.0). Migrations run in BEGIN IMMEDIATE transactions with a busy timeout for the same multi-process reason.
  • Token-shaped responses — the scarce resource on the MCP path is response tokens, not query milliseconds (search measures 1–3 ms even at 2,000 files). Responses carry nothing an agent can’t act on: no debug fields, adjacency maps instead of repeated path serialization, capped lists with explicit truncation flags. See the audit notes.
  • ESM only"type": "module" with NodeNext resolution.
  • Cache correctness over performance — never return stale data; when in doubt, re-read the file. This applies to discovery too: search matches are hash-validated against disk before being served.