Store Internals
How the persistent SQLite semantic index is laid out, keyed by content hash, and updated incrementally.
Indexing a workspace is repeatable work: the same source always yields the same symbols, chunks, and wiring edges. Fuse persists that derived graph in one SQLite file so a warm call reads precomputed structure instead of re-analyzing, and an edit re-indexes only the changed files. This page documents where the store lives, its layout, how content hashing drives incremental updates, and how the reduction cache fits in.
This page is for maintainers working on the store and for engineers diagnosing why a call did or did not see fresh data.
Implementation context
The store trades disk for compute. File records carry a content hash, so an edited file is detected and its derived rows are replaced; an unchanged file is skipped. Because every row is derived from source, a lost or corrupt store costs only a rebuild, never a wrong answer.
Location and layout
All semantic index data lives in a single SQLite database file named fuse.db. Placement depends on whether the workspace is inside a git repository:
| Context | Path |
|---|---|
| Inside a git repo | {repoRoot}/.fuse/fuse.db at the directory that contains .git, not under the scoped subdirectory or process working directory |
| Outside a git repo | ~/.fuse/fuse.db (override the directory with the FUSE_USER_DATA environment variable) |
Derived cache data (reduction output and per-run analysis index) lives in a sibling file fuse-cache.db in the same .fuse/ directory (or ~/.fuse/fuse-cache.db outside a repo). Corruption recovery on the cache file recreates only that database; it never deletes fuse.db.
The file uses WAL journal mode with synchronous = NORMAL and foreign keys on. It is a relational schema, not a key-value cache: separate tables hold files, projects, symbols, chunks, semantic nodes, typed edges, routes, DI registrations, and options bindings, plus an FTS5 virtual table over chunk text (path, name, declared symbols, signature, comments, body, and a subtokens field that holds the subword expansion of the chunk's identifiers so a prose query word matches a compound name). A schema_version row gates migration: a database older than the current version is dropped and rebuilt rather than migrated in place, so adding a full-text column (such as subtokens) is a version bump that rebuilds the index on the next run, with no incremental migration.
The files table carries a language column tagged from the provider that claims each file's extension (for example csharp, python), so retrieval can filter or blend by language over the language-agnostic tables; symbols and nodes inherit their language through their file_id. Adding the column is a schema version bump (currently 14), so the index rebuilds on the next run.
A git_cochange table holds mined file-pair couplings (path_a, path_b, count, PMI, Jaccard, last-seen date). It is populated at index time by a bounded git-history miner: one git log --no-merges --name-only pass over a capped commit window, counting how often each pair of source files changed in the same commit, skipping wide commits (sweeps) and dropping one-off pairs to bound the pairwise blow-up. The single git invocation has a fixed argument list (a commit cap, no variable path list), so the external-process command line is bounded by construction. Mining is best-effort: a non-repository directory or a missing git executable simply leaves the table empty, and the co-change prior is then a no-op. A re-index clears and rewrites the table, so a fresh mine is authoritative.
Content hashing and incremental update
Each file row stores an XXHash64 of its content. When the indexer revisits a workspace, a file whose hash is unchanged is left as is; a file whose hash changed has its prior rows cleared (symbols, chunks, nodes, edges, and FTS entries for that file) and re-extracted. Because the file row is the foreign-key parent, clearing and re-inserting a single file's derived rows keeps the graph consistent without a full rebuild. A removed file's rows are deleted. This is what makes a warm re-index cost a function of the changed files rather than the whole tree.
Symbol and chunk identity
Symbols carry a stable id so the same declaration keeps its identity across re-indexes: symbol:{assembly}:{kind}:{hash} from the Roslyn documentation-comment id when the workspace loads semantically, and a source-only fallback id (symbol:fallback:{path}:{kind}:{name}:{line}) when it does not. Chunks carry a text hash and a token estimate so the renderer can plan a budget without re-reading the file, and the FTS5 row for a chunk is maintained on every upsert and delete so full-text search never returns a stale chunk.
Concurrency
Writes within an index batch run in one transaction; reads use pooled connections and a per-connection busy timeout. Across processes, safety comes from WAL mode and content-addressed rows: because a row is derived from a content hash, a concurrent rebuild can only recompute the same bytes, never produce a conflicting answer. The MCP server and the host hold one store open across calls so the warm graph is shared by every call in the session.
The reduction cache
Rendering a planned file to a token-reduced form is also repeatable, and that reduced output is cached so an unchanged file under the same reduction level is not reduced twice. The reduction cache is keyed by a content hash combined with a hash of the reduction options, so any change to the file or the level yields a distinct entry and a stale entry can never be served. It lives in .fuse/fuse-cache.db (a kv table), separate from the semantic index, and is derived data like the per-run analysis index.
What this does not cover
This page documents the store and its update model. It does not document the analyzers that produce the edges (see The Fuse pipeline) or the retrieval that reads them (see Retrieval internals).
Next
See The Fuse pipeline for where indexing and rendering sit, Performance for cold-versus-warm timings, and Operator guide for reset and environment variables.