Skip to content

v1.1.3 Codex MCP on WSL /mnt can open same repo via upper/lowercase paths, corrupting .codegraph despite CODEGRAPH_NO_DAEMON #1057

Closed

Description

@yjtdkj

Summary

On @colbymchenry/codegraph@1.1.3, Codex MCP still becomes unstable on WSL2 /mnt/d even with the recommended workaround from #974:

[mcp_servers.codegraph]
command = "env"
args = ["CODEGRAPH_NO_DAEMON=1", "codegraph", "serve", "--mcp", "--no-watch"]

The new distinguishing signal is that a single Codex parent process spawned two CodeGraph MCP server processes for the same physical repository with different path casing:

Codex parent:
3608352 .../bin/codex
parent cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal

CodeGraph MCP children of the same parent:
3608757 cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
3609744 cwd=/mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal

Both paths point to the same .codegraph/codegraph.db inode on DrvFs:

upper=70:6192449487701361 /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal/.codegraph/codegraph.db
lower=70:6192449487701361 /mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal/.codegraph/codegraph.db

After this, CLI codegraph status reported:

Failed to get status: database disk image is malformed

Killing the repo-matching CodeGraph MCP processes made CLI status/sync recover, but the Codex MCP tool call then failed with:

Error: tool call error: tool call failed for `codegraph/codegraph_explore`
Caused by:
    Transport closed

Environment

  • CodeGraph: 1.1.3
  • Node: v22.19.0
  • Client: OpenAI Codex CLI
  • OS: WSL2 Ubuntu on Windows, project on /mnt/d DrvFs
  • Project path used by shell / parent process:
    /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
  • MCP config:
[mcp_servers.codegraph]
command = "env"
args = ["CODEGRAPH_NO_DAEMON=1", "codegraph", "serve", "--mcp", "--no-watch"]

Observed process state

pwd and pwd -P from the active shell both showed the uppercase canonical-looking path:

/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal

The Codex parent process also had the uppercase cwd:

3608352 3608345 Mon Jun 29 15:39:36 2026 .../bin/codex
parent cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal

But the same parent spawned two CodeGraph MCP server processes:

PID 3608757
ppid=3608352
cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
env=CODEGRAPH_NO_DAEMON=1
cmdline=node .../bin/codegraph serve --mcp --no-watch

PID 3609744
ppid=3608352
cwd=/mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal
env=CODEGRAPH_NO_DAEMON=1
cmdline=node .../bin/codegraph serve --mcp --no-watch

The Codex MCP tool call also carried the lower-case project path:

codegraph.codegraph_explore({
  "projectPath": "/mnt/d/02_softwarecodes/rcss.studio/tekla/tekladrawinghelper_teklainternal",
  "query": "RunCurrentVersionSwitch CurrentVersionSwitchService ExecuteBatch ValidateBatchPreconditions RecordBatchNotExecuted DrawingTransferManagerForm CanExecuteBatchVersionSwitch DrawingTransferPlanRecord IsExecutableRecord",
  "maxFiles": 5
})

Daemon-related observation despite CODEGRAPH_NO_DAEMON=1

Even with CODEGRAPH_NO_DAEMON=1 in the MCP server environment, .codegraph/daemon.pid contained a relocated daemon:

{
  "pid": 3607983,
  "version": "1.1.3",
  "socketPath": "/tmp/codegraph-befc5052950cc73f.sock",
  "startedAt": 1782718772179
}

Process:

PID 3607983
cwd=/mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal
env=CODEGRAPH_DAEMON_INTERNAL=1
cmdline=.../codegraph.js serve --mcp --path /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal

.codegraph/daemon.log included:

[CodeGraph daemon] Socket /mnt/d/02_SoftwareCodes/RCSS.Studio/Tekla/TeklaDrawingHelper_TeklaInternal/.codegraph/daemon.sock unusable (ENOTSUP); relocating to /tmp/codegraph-befc5052950cc73f.sock.
[CodeGraph daemon] Listening on /tmp/codegraph-befc5052950cc73f.sock (pid 3607983, v1.1.3). Idle timeout 300000ms.
[CodeGraph MCP] File watcher disabled — project is on a WSL2 /mnt/ drive, where recursive fs.watch is too slow to be reliable. The graph will not auto-update; run `codegraph sync` (or install the git sync hooks via `codegraph init`) to refresh.

This may be expected under some path, but it was surprising because the user-facing MCP process had CODEGRAPH_NO_DAEMON=1.

Symptoms

  1. codegraph sync was run.
  2. codegraph status then failed:
Failed to get status: database disk image is malformed
  1. Multiple CodeGraph MCP processes existed, including two for the same physical repo via upper/lower casing.
  2. After killing repo-matching processes, CLI status recovered and sync completed.
  3. The subsequent Codex MCP call failed:
Error: tool call error: tool call failed for `codegraph/codegraph_explore`
Caused by:
    Transport closed

Why this seems distinct from #974

#974 was on v1.1.0 and the recommended workaround was CODEGRAPH_NO_DAEMON=1 to skip the shared daemon on WSL2 /mnt.

In this case:

  • CodeGraph is 1.1.3.
  • The MCP server process has CODEGRAPH_NO_DAEMON=1.
  • The MCP server also has --no-watch.
  • The same Codex parent process spawns two CodeGraph MCP children for the same physical repo with different path casing.
  • The same SQLite database inode is shared by those two path spellings.
  • A relocated internal daemon still appears in .codegraph/daemon.pid.
  • The index can become malformed, then CLI recovers after killing repo-matching MCP processes, while MCP still returns Transport closed.

Expected behavior

On WSL2 /mnt / DrvFs, CodeGraph should either:

  • canonicalize/case-fold project roots that point to the same physical directory before deciding project identity and .codegraph ownership, or
  • lock the SQLite database robustly across equivalent path spellings, or
  • fail fast with an actionable error when the same physical .codegraph directory is opened through multiple path spellings.

It should not allow two MCP processes from the same client parent to treat upper/lowercase spellings of the same DrvFs path as separate project roots while writing the same SQLite database.

Questions

  1. Is CODEGRAPH_NO_DAEMON=1 expected to fully prevent creation/use of a shared daemon in v1.1.3, or can a relocated CODEGRAPH_DAEMON_INTERNAL=1 process still appear?
  2. Does CodeGraph currently canonicalize WSL DrvFs paths by physical identity, or only by path string/realpath?
  3. Would you prefer this be fixed in CodeGraph project-root identity handling, or should MCP clients like Codex avoid sending projectPath values that differ in casing from the server process cwd?

I can provide additional command transcripts if useful.

Activity

  1. inth3shadows commented on Jun 30, 2026

    @inth3shadows
    Contributor

    Opened #1082 with a fix for this.

    Short version: the MCP server caches each open SQLite connection keyed by the resolved-root path string, so two spellings of one repo (an upper/lowercase /mnt path on WSL, or a symlinked checkout) become two cache keys → two connections to the same .codegraph/codegraph.db → corruption. The PR keys the cache on (dev, ino) filesystem identity instead, which is identical for every spelling. Worth noting: realpathSync alone does not fix the case-variant half — on a case-insensitive, case-preserving filesystem it returns the caller's casing — which I verified on a real WSL DrvFs mount. Reproduced before/after on WSL2; added a deterministic symlink-based regression test that runs on case-sensitive CI too.

    One process question, not specific to this issue: for WSL / /mnt-specific bugs like this, do you prefer a PR (as here) or an issue comment with the repro + diagnosis? Happy to match whichever keeps your review queue lightest.

  2. inth3shadows commented on Sep 8, 2026

    @inth3shadows
    Contributor

    Still reproduces on main @ ee83636. Flagging one thing before this gets folded into the new writer lock — it isn't covered by it.

    src/mcp/writer-lock.ts (#1744) is a cross-process pidfile, acquired once per server at startup (daemon.ts:210, engine.ts:294, index.ts:373) to decide who owns live auto-sync. #1057 is one process opening two SQLite connections to one database through two spellings of the path, inside ToolHandler's cache. The lock cannot see that — a second acquire from the same pid returns {kind:'acquired'} by design ("Same process already holds it").

    The keying that causes it is untouched on main:

    src/mcp/tools.ts:1772   this.cg.getProjectRoot() === resolvedRoot     ← string equality
    src/mcp/tools.ts:1779   this.projectCache.get(resolvedRoot)
    src/mcp/tools.ts:1783   this.projectCache.set(resolvedRoot, cg)
    

    findNearestCodeGraphRoot returns a cased path string, so on a case-insensitive mount two spellings of one repo become two cache entries, and a second connection to the same .codegraph/codegraph.db.

    A separate note on the writer lock itself

    Found while checking the above. getWriterPidPath normalizes with fs.realpathSync, and that does not converge on a case-insensitive mount. Probed today on WSL2 DrvFs:

    case-insensitive mount : true
    dev:ino  cg1082probe   : 68:128915539333482220
    dev:ino  CG1082PROBE   : 68:128915539333482220   ← same directory
    realpath cg1082probe   : /mnt/c/Users/Public/cg1082probe
    realpath CG1082PROBE   : /mnt/c/Users/Public/CG1082PROBE
    realpath converges     : false
    

    So two direct-mode servers started at two spellings of one repo each write their own writer.pid under a different .codegraph spelling, and both acquire — the gap #1740 set out to close, reappearing one layer up. Happy to open that as its own issue rather than leave it buried here; say the word and I will.

    #1082 collapses every spelling onto a single dev:ino key, which is the identity that actually converges. It has been open since 2026-06-30 and GitHub currently reports it DIRTY/CONFLICTING — I'll refresh it against main if it's still wanted.

  3. inth3shadows commented on Sep 8, 2026

    @inth3shadows
    Contributor

    Correcting myself: the writer-lock half of my earlier comment is wrong. Please disregard it.

    I claimed that because getWriterPidPath normalizes with fs.realpathSync, and realpath does not converge on a case-insensitive mount, two direct-mode servers started at two spellings of one repo would each write their own writer.pid and both acquire. I inferred that from the realpath behaviour without testing the acquire itself. Tested now, against main @ b8d46d1, two separate processes on WSL2 DrvFs /mnt/c:

    process 1   /mnt/c/…/CgWriterLock   ->  kind=acquired
    process 2   /mnt/c/…/cgwriterlock   ->  kind=taken      <- correctly refused
    control     same spelling again     ->  kind=taken
    

    The lock holds. The reason is that the pidfile path being a different string does not make it a different file: on a case-insensitive mount both spellings resolve to one inode, so the linkSync / O_EXCL create hits EEXIST whichever spelling asks.

    wrote via CgWL2, readable via cgwl2      : true
    O_EXCL create through the other spelling : EEXIST
    

    The filesystem does the deduplication that realpath doesn't, and for a symlinked checkout realpath converges anyway. So there is no writer-lock defect here and nothing to file — my apologies for the noise.

    What this does not change: everything else in that comment stands, and I re-verified it at b8d46d1 rather than assuming it survived the day's commits:

    tools.ts:1772   this.cg.getProjectRoot() === resolvedRoot     <- still a path string
    tools.ts:1779   this.projectCache.get(resolvedRoot)
    tools.ts:1783   this.projectCache.set(resolvedRoot, cg)
    

    projectCache is still keyed by the path string, two spellings still produce two entries, and the measured 3 connections for 2 projects through the real ToolHandler is unaffected — that path never touches the writer lock.

  4. colbymchenry commented on Sep 27, 2026

    @colbymchenry
    Owner

    Thanks for the detailed report, @yjtdkj, and thanks for using CodeGraph! This is fixed on main by #2020, which builds on @inth3shadows's #1082 (thank you!): the MCP server now recognises two spellings of the same repository (a symlink, or different casing on a case-insensitive drive like WSL /mnt/c) as one index and shares a single database connection, instead of opening it twice. The fix will be in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions