← Home

Solograph, rebuilt: one Rust binary over SQLite instead of a graph database and vectors

The short version

project-solograph is the memory my coding agents share: where something is defined across 77 of my repositories, what was said in about a thousand past Claude Code sessions, what my wiki already knows. Until this October it was a FalkorDB code graph with vector search on top, served by Python. It looked impressive and did not work: session search had failed on every call for seven weeks, the nightly scan had indexed zero projects for 70 nights while logging “done”, and knowledge-base search took 14 seconds cold.

Over two days I measured every part of it, cut what the numbers did not defend, and rewrote what was left as one Rust binary over SQLite. It now answers in milliseconds and finds what it used to miss.

Before (Feb to Oct 2026) Now (0.10)
Moving parts FalkorDB, an embedding model (torch), a Python MCP server, a Python CLI, three separate wiki indexes one 34 MB binary, a few SQLite files
Python environment 862 MB 34 MB, scrapers only
Sessions found, 39 with a known answer, top 5 0 32
Knowledge-base search 14 s cold 12 ms median
Russian question for an English wiki page, top 5 of 22 0 21 (19 of 22 on a set written after tuning)
Prompt hook that points at the wiki 74 ms, a Python wrapper 19 to 35 ms, the binary itself
Nightly scan scanned 0 projects for 70 nights, said “done” fails loudly, checked every morning

The whole thing is open source: fortunto2/solograph, MIT.

Words you’ll need

Term What it means here
MCP Model Context Protocol: how Claude Code and other agents call outside tools. Solograph is an MCP server with 5 tools (12 when this was written; 0.17 cut them, see project-solograph).
tree-sitter A parser library with grammars for most languages. Solograph uses it to read function, class and protocol definitions out of 15 languages.
SQLite FTS5 SQLite’s built-in full-text index. No server, one file, and it ranks matches with BM25.
BM25 The standard ranking formula of lexical search: a match on a rare word counts more than a match on a common one, and a long document does not win just by being long.
Stemming Cutting a word to its stem, so “графами” finds “граф” and “indexing” finds “index”. Snowball does it for Russian and English.
Alias table A hand-kept list of other spellings of a term the wiki uses: [песочница, sandbox]. It is how a Russian question reaches an English page without embeddings.
Known-answer eval A fixed list of queries with the right answer next to each one. Every ranking change has to move its numbers, or it does not ship.
Hook A command Claude Code runs on an event. A UserPromptSubmit hook sees every prompt before the model does.

What it is now

One binary does four jobs: it is the MCP server agents talk to, every read as a terminal command (with --json for the exact answer the agent would get), the hook that points a prompt at the wiki, and the nightly scan. Everything it knows sits in SQLite files under ~/.solo/.

flowchart LR
    subgraph Disk["What is already on disk"]
        R["77 git repositories"]
        T["Claude Code transcripts
(deleted after 30 days)"] W["Markdown knowledge base
wiki/, inbox, CRM notes"] Y["YouTube, ProductHunt,
TrustMRR scrapers (Python)"] end subgraph Bin["solograph: one Rust binary"] S["tree-sitter scan"] F["FTS5 + BM25 + stems"] end subgraph DB["SQLite under ~/.solo"] P[("portfolio.db
63k definitions")] SE[("sessions
977 indexed, 638 archived")] K[("kb.db
516 documents")] SO[("sources")] end R --> S --> P T --> F --> SE W --> F --> K Y --> SO P & SE & K & SO --> O["MCP: 5 tools for agents"] P & SE & K & SO --> C["CLI: the same reads"] K --> H["Prompt hook: 'the wiki has notes on this'"] K --> G["Pre-commit: orphans, broken links"]

There is no reindex step to forget. Before answering, each index checks the files it covers by modification time and size and re-reads only what changed. Transcripts only ever grow, so a grown one is read from the byte where the last read stopped; live transcripts reach 400 MB and a resync costs about 10 ms. When Claude Code deletes a transcript after 30 days, the index keeps its text and marks it transcript: deleted, so session search is the only copy of those conversations left anywhere.

flowchart TD
    Q["A search arrives"] --> St["stat every covered file
(mtime, size)"] St --> U{"changed?"} U -- "no" --> A["answer from the index"] U -- "new or edited" --> RR["re-read that file"] --> A U -- "transcript grew" --> AP["read only the new tail,
from the saved byte offset"] --> A U -- "transcript deleted" --> AR["keep its text,
mark it archived"] --> A

What was cut, and the number behind each cut

The vectors. Session search with embeddings found 0 of 39 sessions that had a known answer; FTS5 found 32. The vector index held 640 sessions and the disk held 633, with zero overlap: Claude Code had deleted the old transcripts and nothing re-read the disk. Knowledge-base search took 14 seconds cold, and its “relevance 1.0” on every top hit turned out to be rank position. Source search hung past two minutes.

I had written the opposite advice myself a month earlier, in graphify-vs-solograph: keep embeddings for the Russian-language knowledge base, because a graph cannot index prose. The measurement disagreed. A table of 315 alias groups took Russian questions to English pages from 0 of 22 to 20 of 22, with nothing to download and nothing to drift.

The structural graph. 781,000 call edges, plus imports, inheritance and session-to-file edges. In four months agents called it five times, and every call was “list the projects” or stats. To check whether a better graph would change that, I ran colbymchenry/codegraph, the cleanest of the current code-graph servers, against plain Grep and Read on a 1,100-file Rust and Swift app, with claude -p and the method from its own README. Same accuracy in all 8 runs. It saved 16% on the one question that needed exploration ($1.73 against $2.07) and nothing on the other. Inside one repository, grep is enough; what grep cannot do is look across 77 repositories at once, so that part stayed. The survey of 15 such tools is in code-graph-mcp-landscape.

The second implementation. For a day the same 12 tools existed in Rust and in Python. Two implementations of one search drift, and drift is exactly what had kept session_search broken for seven weeks (a return-type annotation that did not match what the function returned, with tests that never went through the protocol). Every read moved into the binary. Python kept only the scrapers that fill the sources index.

flowchart LR
    subgraph Before["Before"]
        FK["FalkorDB
781k call edges"] V["embeddings
(torch, 862 MB env)"] PY["Python MCP + Python CLI
two copies of every read"] WI["3 more wiki indexes:
search, hint, link graph"] end subgraph After["After"] B["one Rust binary
SQLite FTS5"] end FK -- "0 structural questions in 4 months" --> X1["cut"] V -- "0 of 39 sessions found" --> X2["cut"] PY -- "drift broke session search for 7 weeks" --> X3["merged"] --> B WI -- "the link graph sat at 36 of 160 pages" --> X4["merged"] --> B

The knowledge base: one index for search, links and the hint

My wiki follows Karpathy’s LLM wiki pattern: raw sources stay as they are, and an agent maintains interlinked pages on top. Until this week the same pages were indexed four times, by four tools that disagreed: solograph for agents, a Python index for make wiki-search, a third for the prompt hint, and a JSON link graph a script rebuilt by hand. The link graph had 36 nodes, frozen since February, while the wiki passed 160 pages.

Now one SQLite index serves all of it. Search ranks the title and a page’s one-line summary twice as high as its body. The link graph is read from the same pass over the files, so solograph kb-graph --check (orphan pages, broken links, pages missing from the catalog) is always as fresh as search, and its exit code is the pre-commit gate. The hint is the binary itself, registered as a UserPromptSubmit hook: when a prompt names something the wiki is specifically about, the session gets one line pointing at the page, and otherwise nothing.

flowchart LR
    Q["'песочница для агента
на маке'"] --> ST["stems:
песочниц*, агент*, мак*"] ST --> AL["alias table:
песочница → sandbox"] AL --> M["FTS5 MATCH
its own words OR sandbox*"] M --> RK["BM25, weights 2 / 2 / 1:
title, summary, body"] RK --> SC["scope: the wiki folder
(.solograph.yaml)"] SC --> OUT["wiki/agent-sandboxing.md"] OUT -. "optional" .-> NB["+ up to 3 pages
linked from the hits"]

Every layer stayed only if it moved a known-answer set. The sets were built from the wiki itself (three content words of a random page, three words of its summary line), plus 22 Russian questions for English pages and 10 real queries agents had sent.

Layer body words, #1 of 40 summary words, #1 of 25 real agent queries, #1 of 10 Russian → English, top 5 of 22
title + body index (before) 29 18 7 0
title, summary, body as fields, BM25 2-2-1 31 22 7 0
+ alias table, one way 31 22 8 4
scoped to the wiki folder 33 22 9 21

Two things did not survive. Aliases in both directions sent English queries to Russian documents (top-1 on the English set fell from 31 to 24), so they expand one way only: from the other spellings to the term the wiki writes. And the weights the knowledge agent I built at Epiphan used, 5-3-1, lost to 2-2-1, as did every title weight from 1 to 12. To check that the Russian result was not tuned to its own test, I wrote 22 new Russian questions afterwards and ran them once: 19 of 22 in the top 5.

The engine no longer knows anything about my folder layout. A knowledge base states it in .solograph.yaml: where the wiki is, where stack templates live, what to skip. Skipping one gitignored client checkout, which was 949 of the 1,187 directories every search walked, took search from 60 ms to 12.

What I took from other repositories

Almost everything here exists elsewhere in some form. The field converged on tree-sitter plus SQLite FTS5 with embeddings optional, as the survey in code-graph-mcp-landscape shows; solograph arrived at the same place by measuring its own failures. What came from where:

From What it gave
DeusData/codebase-memory-mcp Split identifiers on camelCase and snake_case before indexing, so “montage profile” finds both resolve_montage_profile and MontageProfile. Also the proof that a single static binary is enough.
colbymchenry/codegraph The benchmark method: claude -p with only the tool’s MCP server against no MCP at all, same questions, answers checked against ripgrep. It is what retired my call graph.
trailhq/Trail-Code Every query tops the index up first. That is the sync-before-read above, and why there is no reindex command to forget.
Graphify-Labs/graphify Put the reminder in a hook, not in a rule: Graphify intercepts file reads to suggest its graph. My version is the prompt hook pointing at the wiki. The broader principle is agent-mistake-fix-harness.
probelabs/probe “Agents don’t need embedding search”: BM25 over the right fields is enough for code and notes.
The knowledge agent I built at Epiphan Field-weighted BM25, a term-alias table instead of embeddings, linked pages capped at 3, and the rule that each layer ships only with its measured delta.
Karpathy’s LLM wiki The shape of the knowledge base being searched.
oraios/serena Not taken, on purpose: exact references through language servers are a complement for deep work in one repository, not a replacement for a portfolio index.

Set up the same for yourself

You need Claude Code (or any MCP client) and macOS or Linux. The commands below were run on a clean home directory before this was published.

1. The binary. Either a prebuilt one from the latest release:

mkdir -p ~/.solo/bin
curl -fL -o ~/.solo/bin/solograph \
  https://github.com/fortunto2/solograph/releases/latest/download/solograph-macos-arm64   # or solograph-linux-x86_64
chmod +x ~/.solo/bin/solograph

or built from source, a few minutes with a Rust toolchain:

cargo install --git https://github.com/fortunto2/solograph solograph --root ~/.solo

2. Give it to your agent, pointing it at the folder that holds your notes:

claude mcp add -s user solograph -e KB_PATH=$HOME/notes -- ~/.solo/bin/solograph

3. Index your projects: every git checkout under the folders you name.

CODEGRAPH_SCAN_PATH=~/projects ~/.solo/bin/solograph scan
~/.solo/bin/solograph symbols "auth middleware"      # what the agent will get

Sessions need nothing: the index reads ~/.claude/projects on the first search. For new code by morning, add both steps to cron or launchd: 0 4 * * * ~/.solo/bin/solograph registry && ~/.solo/bin/solograph scan. registry adds checkouts that appeared; scan only reads the registry, so a project you removed from it stays out.

4. A knowledge base, if you keep one: a folder of markdown with YAML frontmatter, the linked pages in wiki/. If your layout differs, say so in .solograph.yaml at its root:

kb:
  pages: wiki/                 # [[links]], index.md, _aliases.yaml live here
  exclude: [archive, drafts]   # a bare name anywhere, or a path from the root
cd ~/notes
~/.solo/bin/solograph kb --pages "privacy architecture"
~/.solo/bin/solograph kb-graph --check    # orphans, broken links; exit 1 when any

If you write in two languages, add wiki/_aliases.yaml, one group per line, the term your pages use last: - [песочница, sandbox].

5. The prompt hint (optional), in ~/.claude/settings.json:

"hooks": {
  "UserPromptSubmit": [
    { "hooks": [ { "type": "command", "timeout": 3,
      "command": "$HOME/.solo/bin/solograph kb-hint --hook --kb-path $HOME/notes" } ] }
  ]
}

It stays silent unless a prompt names something your wiki is specifically about, and it never blocks a prompt: any failure is silence.

What is still open

Neither failure was visible from the outside. The vector index answered every query, and the graph held 781,000 edges. Both came out the day I wrote down the right answers to 39 session questions and 75 wiki queries and ran them.

How this was measured

Related

Related