NxtSoftLabs
← All writing

Fold-in vs full rescan: incremental code graph indexing

August 10, 2026·6 min read

A warm code graph is only useful if it stays current. CGraph's resident daemon keeps its graph fresh with two moves: ordinary edits are folded in incrementally — re-parsing only the files whose content actually changed, usually within a couple of seconds — while a large batch of changes, like a branch switch, collapses into one full rescan, because rebuilding once is cheaper than folding in a whole tree's worth of edits. This post walks through how the daemon decides between the two, what a fold-in actually recomputes, and the crash-safety net underneath it.

Every number below is a default from the daemon's public source (daemon_server.hpp), not an estimate.

Why not just rebuild every time

CGraph's whole premise is that impact analysis should take ~10ms, which requires a graph that is already resident in memory when the question arrives. A full build is the expensive part — walking the tree, running tree-sitter over every source file, resolving references, deduplicating, computing communities and centrality. Doing that after every save would turn a warm daemon back into a batch tool.

So the daemon builds once, publishes a baseline graph, and from then on treats your edits as deltas. The interesting engineering is in keeping the delta path both fast and convergent with what a from-scratch build would produce.

The watch loop: poll, debounce, classify

The watcher is a poller, not an OS-event subscriber: on its own cadence it walks the project tree looking for changed source files. That walk has a real cost on a large repo, so it runs every 2 seconds — separate from the faster poll the daemon uses for enrichment drop directories — and it only starts once the initial build has published a baseline graph. There is nothing to fold changes into before that.

Two guards sit between "a file changed on disk" and "the graph mutates":

  • Debounce. A save burst on one file — an editor writing, a formatter rewriting, a linter fixing — coalesces into a single event after 250ms of quiet, instead of triggering three extractions of the same file.
  • Batch threshold. If more than 256 file events are pending at once, the daemon stops treating them as edits. That volume means something tree-shaped happened — a git checkout, a branch switch, a generated-code refresh — and the batch collapses into a single full rescan.
edit on disk
save, checkout, delete
watcher poll
every 2s
debounce
250ms coalesce
count pending
≤ 256 · > 256
fold-in or rescan
publish new snapshot
The watch loop. Small batches fold in; more than 256 pending events collapse into one full rescan.

What a fold-in actually does

For each changed file, the fold-in path is aggressively lazy:

  1. Content check first. The file's SHA-256 is compared against the hash stored with its previous extraction. A touch, a re-save with no changes, or a checkout that restored identical content is a cache hit — no parsing happens at all.
  2. Re-extract only real changes. A file whose content genuinely differs is re-parsed with tree-sitter — that one file, not its importers, not its directory.
  3. Deletions shrink the graph. A removed file's nodes and its cache entry are dropped, so the graph doesn't accumulate ghosts of deleted code.
  4. Rebuild and re-rank. The graph is reassembled from the per-file index, then communities and centrality are recomputed, and the new snapshot is published atomically — queries never see a half-updated graph.

Deduplication gets a special treatment, because it's the step that most wants global knowledge. A full build runs fuzzy duplicate-merging across the entire graph; doing that on every save would defeat the purpose. So a fold-in runs neighborhood dedup — only around the files that changed — and every 5th incremental update runs the full pass to reconcile any drift, so the incremental graph stays convergent with what a canonical from-scratch build would produce. An explicit update op or a restart rescan also reconverges it.

One more re-overlay happens after every fold-in: the code rebuild is code-only, so LLM-authored semantic fragments and session-memory checkpoints are re-applied on top of the fresh code graph. Your agent's accumulated knowledge survives your edits.

When a fold-in becomes a full rescan

Past the 256-event threshold — or when the watcher reports an overflow — the daemon runs one full stat-index rescan instead. "Full" here means full coverage, not full cost: the rescan walks the entire tree, but each file still goes through the same content-hash check, so a branch switch where 90% of files are identical re-parses only the 10% that differ. The rescan also recomputes the file-coverage map wholesale and runs the full dedup pass, which is why it doubles as the self-healing path: any drift the incremental path accumulated is gone after one rescan.

The same machinery covers a subtle restart case. After a fast-load start — where the daemon resumes from persisted state instead of rebuilding — the first edit triggers one full rescan to hydrate the per-file index, and every edit after that goes incremental.

2s
watcher poll cadence
250ms
save-burst debounce
256
pending events before a full rescan
30s
background persist interval
Defaults from DaemonServerOptions in the public source — cadence, coalescing, the fold-in/rescan tipping point, and crash-safety persistence.

Crash safety: persist every 30 seconds

Incremental changes mutate the in-memory graph first. To bound what a crash can lose, the daemon re-persists the graph and its index manifest to cgraph-out/ in the background — at most 30 seconds of edits are ever memory-only — and again on shutdown. The next start resumes from the current graph rather than a cold build. (How those writes are made atomic, so a crash mid-persist can't corrupt the graph, is its own story: hardening a local daemon.)

While a build or rescan is still underway, queries return graph_state: "building", so a caller — human or agent — can tell "not ready yet" apart from "genuinely no results".

Explicit control

Watching is on by default and --no-watch turns it off. With watching off, or any time you want a refresh now, ask the daemon directly:

cgraph-client update '{"path":"."}'

Agents get the same lever over MCP as graph_update — one of the eight MCP tools.

Why this matters for AI coding agents

An AI coding agent edits code continuously, and every edit silently invalidates part of the graph it's querying. If the graph lags, impact analysis returns yesterday's blast radius — confidently wrong, which is worse than slow. Fold-in latency of a couple of seconds means that by the time an agent finishes writing a file and formulates its next query, the graph already reflects the change — no update call, no rebuild step, no stale answer. Combined with a supervisor that keeps every repo's daemon warm, the graph becomes something an agent can simply trust to be current.

Try it

Start a daemon in any repo, edit a file, and query it two seconds later:

graphd --root .
# edit src/whatever.ts, then:
cgraph-client query '{"q":"whatever"}'

The watcher, the fold-in path, and the rescan collapse are all open source — read the code on GitHub. For the reference version of this behavior, see the incremental updates docs; for what else the daemon does while it's resident, start with the daemon docs.