Skip to content

Project walk-up resolves $HOME as a project because $HOME/.codegraph is also the app-state dir #2

Closed

Description

@inth3shadows

Summary

.codegraph/ means two different things, and the project resolver cannot tell them apart:

  • <repo>/.codegraph/ — a project index
  • $HOME/.codegraph/ — codegraph's own app state (telemetry.json, telemetry-queue.jsonl, update-check.json, daemons/, daemon.sock, daemon.pid)

Because the project resolver walks up from $PWD looking for .codegraph/, any session whose cwd is under $HOME and which has no index of its own terminates the walk at $HOME/.codegraph and resolves $HOME as its project root. It then takes daemon mode (decision order step 4, serve --mcp), spawns the full 4-process chain, fails to open the non-existent index, and stays resident for the life of the session.

Symptom

daemon.log, repeating indefinitely across restarts:

[CodeGraph daemon] Listening on /home/ericm/.codegraph/daemon.sock (pid 701885, v1.6.0). Idle timeout 300000ms.
[CodeGraph MCP] Failed to open project at /home/ericm: no such table: unresolved_refs
[CodeGraph MCP] Failed to open project at /home/ericm: no such table: unresolved_refs

$HOME/.codegraph/codegraph.db is 4096 bytes — an empty SQLite file created once and never populated.

Why it cannot be worked around by deleting it

Moving $HOME/.codegraph aside works for about four minutes:

09:40  mv ~/.codegraph ~/.codegraph.disabled-20260917
09:43  ~/.codegraph/update-check.json      recreated
09:44  ~/.codegraph/telemetry-queue.jsonl  recreated

App state is recreated on every run, so the directory that poisons the walk-up is guaranteed to exist whenever codegraph is in use. It can only be ignored, never removed.

Impact — this is a memory bug, not just a log-noise bug

Every Claude Code session spawns its own codegraph process chain. Measured on one machine (v1.6.0, Node 22.23.2, WSL2):

mode                                        procs   RSS
daemon  (.codegraph/ resolved)                4     284M
direct  (no .codegraph/ reachable)            2     131M

A session under $HOME with no index of its own pays the 284M daemon-mode path — the most expensive one — for a project that cannot be opened. The correct behaviour would be direct mode at worst, and arguably "no project, no server".

At 20 concurrent sessions that is the difference between fitting and not fitting in a 24GB VM.

Reproduce

mkdir -p ~/scratch-no-index && cd ~/scratch-no-index   # any dir under $HOME with no index
codegraph serve --mcp
# -> resolves $HOME as the project, daemon mode, "no such table: unresolved_refs"

Contrast with a cwd outside $HOME (e.g. /tmp/... when /tmp/.codegraph does not exist), which correctly falls to direct mode and logs:

[CodeGraph MCP] No .codegraph/ at or above <dir>: no default project, live sync disabled.

That is the message every no-index session should produce.

Suggested fix

Ranked by how much they change:

  1. Skip $HOME/.codegraph in the project walk-up. One condition, fixes the bug, no migration, no layout change.
  2. Separate the two namespaces. App state belongs in $XDG_STATE_HOME/$XDG_CACHE_HOME (~/.local/state/codegraph, ~/.cache/codegraph); .codegraph/ stays a project marker only. This is the real fix and also resolves upstream Feature request: XDG directory spec support colbymchenry/codegraph#924, which asks for XDG support on tidiness grounds and does not mention this failure mode.
  3. Treat an empty index as absent. A codegraph.db of <= 8KB has no tables; resolving to it guarantees the unresolved_refs failure. Walking past it (or failing to direct mode) is strictly better than starting a daemon for it. A .codegraph/ with no codegraph.db at all is a freshly initialised project and should still be honoured.

(1) and (3) are independent and can ship together ahead of (2).

Workaround in use here

A wrapper in front of the MCP command that performs the walk itself, skips $HOME, rejects indexes whose db is <= 8KB, and otherwise execs the real binary — falling back to the real binary on any unexpected condition. Where no real index is found it serves a minimal MCP server advertising zero tools (~11M), so the client handshake still completes.

Measured on this machine: $HOME-rooted sessions went from 284M + a permanent error loop to 11M and clean stderr; repos with a real index are unaffected.

Environment

  • codegraph 1.6.0 (fork build v1.6.0-em.1)
  • Node 22.23.2
  • Linux 6.6.87.2 (WSL2), Ubuntu 24.04
  • 52 real project indexes under the tree; $HOME/.codegraph was the only empty one, and the only one causing this

Activity

  1. inth3shadows commented on Sep 17, 2026

    @inth3shadows
    OwnerAuthor

    Correction — the root cause in the report above is wrong

    I filed this having read the symptom but not the resolver. Reading src/directory.ts changes the diagnosis, and therefore the fix.

    What I claimed: the walk-up finds any .codegraph/ directory, so $HOME/.codegraph (app state) captures it.

    What is actually true: findNearestCodeGraphRoot() (src/directory.ts:158-177) already guards via isInitialized(), which requires codegraph.db to be present. App state alone — telemetry.json, update-check.json, daemons/ — does not capture the walk.

    The real defect is that isInitialized() (src/directory.ts:93-101) tests existence of codegraph.db, never that it has a schema:

    const dbPath = path.join(codegraphDir, 'codegraph.db');
    return fs.existsSync(dbPath);

    A codegraph.db that exists with zero tables — a failed or abandoned init — is accepted as a real index and permanently captures resolution for every directory beneath it. On this machine:

    $ stat -c %s ~/.codegraph.disabled-20260917/codegraph.db
    4096
    $ sqlite3 ... "select count(*) from sqlite_master where type='table'"
    0
    

    created 2026-07-15, never populated. $HOME is an ancestor of everything, so it captured every session that had no index of its own.

    Consequently, one more claim above is wrong: I wrote that the directory "can only be ignored, never removed" because app state is recreated. The app state does come back — but without a codegraph.db, so the recreated directory does not capture the walk. Moving it aside genuinely did fix the root cause. That is luck rather than design: anything that puts a db back restores the bug.

    Identical at v1.6.0 and current upstream main (142 commits past the tag).

    Revised fix, in priority order

    1. isInitialized() should validate the db has a schema, not merely exist. This is the actual bug and it is not $HOME-specific — a failed init anywhere poisons every descendant.
    2. Exclude os.homedir() from findNearestCodeGraphRoot(). Defence in depth, and upstream already applies exactly this reasoning to the down-scan in eligibleForSubprojectScan() (src/directory.ts:262-269, citing upstream prompt-hook: monorepo sub-project down-scan escapes cwd when a stray workspace manifest defeats the looksLikeProjectRoot gate ($HOME injects an unrelated project) colbymchenry/codegraph#1454): "The user's home directory and the filesystem root are never eligible."
    3. Split app state out of .codegraph/ into $XDG_STATE_HOME/$XDG_CACHE_HOME. Structural fix; also upstream Feature request: XDG directory spec support colbymchenry/codegraph#924.

    The memory numbers and repro in the original report stand — only the causal explanation was wrong.

    Upstream

    Posted the corrected analysis on colbymchenry#924: colbymchenry#924 (comment)

    Workaround in place here

    codegraph-gate guards on both: it skips $HOME and rejects any index whose codegraph.db is <=8KB, while allowing a .codegraph/ with no db yet (a freshly initialised project). Both guards turn out to be the right ones — (2) and (1) respectively.

  2. inth3shadows commented on Sep 26, 2026

    @inth3shadows
    OwnerAuthor

    Fixed on this fork's main (22a3f5e) by carrying upstream PR colbymchenry#1913 (@danusha2345, merged here as bf37fca, upstream issue colbymchenry#1895). A codegraph.db with no schema no longer counts as an initialized project, so an empty file in an ancestor such as $HOME/.codegraph can't capture resolution any more.

    Reopen if a session under $HOME still resolves $HOME as its project on the fork's main.

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