Skip to content

fix(mcp): dedupe one repo's two path spellings onto one DB connection (#1057)#1082

Open
inth3shadows wants to merge 1 commit into
colbymchenry:mainfrom
inth3shadows:fix/1057-root-identity-cache
Open

fix(mcp): dedupe one repo's two path spellings onto one DB connection (#1057)#1082
inth3shadows wants to merge 1 commit into
colbymchenry:mainfrom
inth3shadows:fix/1057-root-identity-cache

Conversation

@inth3shadows

@inth3shadows inth3shadows commented Jun 30, 2026

Copy link
Copy Markdown

Problem (#1057)

On WSL, opening a repo under /mnt via two different path spellings — most concretely an upper- vs lowercase variant of the same path — corrupts .codegraph, even with CODEGRAPH_NO_DAEMON set. The same class of bug hits a symlinked checkout on any platform.

Root cause

ToolHandler.getCodeGraph caches each open CodeGraph (a live SQLite connection) in projectCache, and reuses the default instance, keyed on the resolved-root path string. Two spellings of one physical directory resolve to two different strings → two cache entries → two SQLite connections to the same .codegraph/codegraph.db. Concurrent writes across the two connections corrupt the index. (This is the same second-connection hazard already documented at the #238 comment in that method.)

Why not just realpathSync the root

That was my first attempt, and it is insufficient — verified on a real WSL DrvFs mount:

realpathSync('/mnt/c/.../MyProj') -> /mnt/c/.../MyProj
realpathSync('/mnt/c/.../myproj') -> /mnt/c/.../myproj   # casing preserved!

realpathSync resolves symlinks and ./.., but on a case-insensitive case-preserving filesystem it returns the caller's casing, so it cannot dedupe case-variants. Filesystem identity (dev, ino) is identical for every spelling and is the robust key.

Fix

Key projectCache and the default-instance reuse check on (dev, ino) via a new canonicalRootKey() helper. This mirrors the inode-identity pattern this codebase already uses in DatabaseConnection.openedInode (statInode) for replace-on-disk detection, so it's idiomatic rather than novel. Minimal blast radius: findNearestCodeGraphRoot itself is unchanged.

Reproduced (WSL2 / Ubuntu)

Before fix, against the built resolver:

[drvfs] via MyProj: /mnt/c/.../MyProj
[drvfs] via myproj: /mnt/c/.../myproj   -> two roots -> two connections (corruption)

After fix, canonicalRootKey converges:

symlink converges:            true
drvfs case-variant converges: true
distinct projects stay distinct: true

Tests

__tests__/root-identity.test.ts (4 tests). The symlink case is a deterministic, filesystem-agnostic proxy for the case-insensitive-mount scenario (both produce two path strings for one inode), so it runs on case-sensitive CI. Full suite green locally (the only failures were two pre-existing CPU-contention timing flakes — query-pool, mcp-daemon — that pass in isolation).

Scope / follow-up

This fixes the in-process connection cache — the reported CODEGRAPH_NO_DAEMON path. The daemon registry (registerDaemon, daemon-registry.ts) keys daemons by path too, so the with-daemon case has the same class of issue; flagging it as a separate follow-up rather than expanding this PR.


Validated on WSL2 (Ubuntu). Happy to adjust naming/placement to your preference.

…ath (colbymchenry#1057)

Two spellings of one repo — a symlinked checkout, or upper/lowercase variants
of a path on a case-insensitive mount (Windows NTFS, WSL DrvFs /mnt) — resolved
to two different cache keys, so the MCP server opened a second SQLite connection
to the same .codegraph/codegraph.db; concurrent writes then corrupted the index.

Key projectCache (and the default-instance reuse check) on (dev,ino) filesystem
identity, which is identical for every spelling. realpath alone is insufficient:
on a case-insensitive, case-preserving filesystem it returns the caller's casing
and cannot dedupe case-variants. Mirrors the existing inode-identity pattern in
DatabaseConnection.openedInode.

Adds __tests__/root-identity.test.ts (symlink case is a deterministic, FS-agnostic
proxy for the case-insensitive-mount scenario).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant