Summary
planFrontload already handles the case where the index lives in a child rather than at the agent's cwd. That is what findIndexedSubprojectRoots() was added for (#964). The MCP server's own root resolution never got the same treatment: resolveDaemonRoot() and MCPEngine.doInitialize() still rely solely on the upward walk in findNearestCodeGraphRoot().
So when an MCP host is launched from a workspace container whose sub-projects are indexed, the server starts, advertises its tools, and then does nothing: no database is opened, no default project is set, and startWatching() is never called. Live auto-sync, the main reason to run the daemon at all, is silently off.
Reproduction
workspace/
├── .mcp.json ← MCP host cwd; NO .codegraph/ here
├── service-a/
│ └── .codegraph/ ← indexed
└── service-b/
└── .codegraph/ ← indexed
- Run
codegraph init inside service-a and service-b, not in workspace/.
- Give the host a plain entry, with no
--path:
{ "codegraph": { "type": "stdio", "command": "codegraph", "args": ["serve", "--mcp"] } }
- Start the agent with cwd =
workspace/, edit and save a source file under service-a.
Expected: the edit is picked up, the same way it is when cwd is the indexed root itself.
Actual: nothing happens.
~/.codegraph/daemons/ never gains an entry for either child root, so no daemon was ever started for them.
- No
daemon.log is created under either child's .codegraph/.
- The MCP tools are listed but have no default project.
- A later manual
codegraph sync from inside a child does pick up the change, which makes the failure look intermittent rather than structural.
Why it happens
resolveDaemonRoot() (src/mcp/index.ts:160) calls findNearestCodeGraphRoot(cwd). The walk only goes up (src/directory.ts:158), so the two indexed children are never seen. It returns null.
null means direct mode, correctly so, since the lock and socket both live under .codegraph/. No daemon is started.
MCPEngine.doInitialize() (src/mcp/engine.ts:201) repeats the same lookup at :204, gets null, and returns early:
if (!resolvedRoot) {
// No .codegraph/ above searchFrom. Sessions may still discover one later via roots/list
this.projectPath = searchFrom;
return;
}
startWatching() at :215 is skipped. retryInitializeSync() (:157) uses the same upward-only lookup, so the retry path cannot recover either. roots/list reports the same container directory, so that fallback does not help here.
The helper that would fix this already exists and is already exported: findIndexedSubprojectRoots() (src/directory.ts:212) is a bounded breadth-first scan (depth 4, max 64 results, heavy directories skipped) that stops at the first indexed directory on each branch. src/mcp/engine.ts:15 currently imports only findNearestCodeGraphRoot.
Suggested fix
Mirror what planFrontload (src/directory.ts:543) already does, in doInitialize (and retryInitializeSync), only when the upward walk returns null:
- exactly one indexed sub-project → adopt it as the default project and watch it;
- several → keep no default, but log the list so the agent knows to pass
projectPath explicitly;
- none → today's behaviour.
The same resolution should feed resolveDaemonRoot, so the adopted root also gets a daemon and the single-watcher, single-writer guarantees rather than a direct-mode server per host.
The cost is one bounded scan on the path where the server currently gives up entirely, and the semantics are already precedented by the front-load hook.
Open question: the workspace-root gate
findIndexedSubprojectRoots is reached in planFrontload only behind looksLikeProjectRoot() (src/directory.ts:196), which requires one of the WORKSPACE_ROOT_MANIFESTS (package.json, go.mod, Cargo.toml, and so on). A container that holds only agent configuration and a .git directory, with no build manifest of its own, fails that gate, so it would still get nothing after this change. That is the exact shape that motivated this report.
Widening the gate to include .git would cover it, but it also turns the down-scan on for essentially every repository, and #1454 already reports the gate misfiring in the other direction. Worth deciding deliberately rather than as a side effect. The diagnostic in the companion issue is useful either way, since it makes the "no project resolved" state visible even when the scan is not attempted.
Environment
CodeGraph 1.5.0, Windows 11, native filesystem (no WSL). Line numbers are against main at the time of writing.
Summary
planFrontloadalready handles the case where the index lives in a child rather than at the agent's cwd. That is whatfindIndexedSubprojectRoots()was added for (#964). The MCP server's own root resolution never got the same treatment:resolveDaemonRoot()andMCPEngine.doInitialize()still rely solely on the upward walk infindNearestCodeGraphRoot().So when an MCP host is launched from a workspace container whose sub-projects are indexed, the server starts, advertises its tools, and then does nothing: no database is opened, no default project is set, and
startWatching()is never called. Live auto-sync, the main reason to run the daemon at all, is silently off.Reproduction
codegraph initinsideservice-aandservice-b, not inworkspace/.--path:{ "codegraph": { "type": "stdio", "command": "codegraph", "args": ["serve", "--mcp"] } }workspace/, edit and save a source file underservice-a.Expected: the edit is picked up, the same way it is when cwd is the indexed root itself.
Actual: nothing happens.
~/.codegraph/daemons/never gains an entry for either child root, so no daemon was ever started for them.daemon.logis created under either child's.codegraph/.codegraph syncfrom inside a child does pick up the change, which makes the failure look intermittent rather than structural.Why it happens
resolveDaemonRoot()(src/mcp/index.ts:160) callsfindNearestCodeGraphRoot(cwd). The walk only goes up (src/directory.ts:158), so the two indexed children are never seen. It returnsnull.nullmeans direct mode, correctly so, since the lock and socket both live under.codegraph/. No daemon is started.MCPEngine.doInitialize()(src/mcp/engine.ts:201) repeats the same lookup at:204, getsnull, and returns early:startWatching()at:215is skipped.retryInitializeSync()(:157) uses the same upward-only lookup, so the retry path cannot recover either.roots/listreports the same container directory, so that fallback does not help here.The helper that would fix this already exists and is already exported:
findIndexedSubprojectRoots()(src/directory.ts:212) is a bounded breadth-first scan (depth 4, max 64 results, heavy directories skipped) that stops at the first indexed directory on each branch.src/mcp/engine.ts:15currently imports onlyfindNearestCodeGraphRoot.Suggested fix
Mirror what
planFrontload(src/directory.ts:543) already does, indoInitialize(andretryInitializeSync), only when the upward walk returnsnull:projectPathexplicitly;The same resolution should feed
resolveDaemonRoot, so the adopted root also gets a daemon and the single-watcher, single-writer guarantees rather than a direct-mode server per host.The cost is one bounded scan on the path where the server currently gives up entirely, and the semantics are already precedented by the front-load hook.
Open question: the workspace-root gate
findIndexedSubprojectRootsis reached inplanFrontloadonly behindlooksLikeProjectRoot()(src/directory.ts:196), which requires one of theWORKSPACE_ROOT_MANIFESTS(package.json,go.mod,Cargo.toml, and so on). A container that holds only agent configuration and a.gitdirectory, with no build manifest of its own, fails that gate, so it would still get nothing after this change. That is the exact shape that motivated this report.Widening the gate to include
.gitwould cover it, but it also turns the down-scan on for essentially every repository, and #1454 already reports the gate misfiring in the other direction. Worth deciding deliberately rather than as a side effect. The diagnostic in the companion issue is useful either way, since it makes the "no project resolved" state visible even when the scan is not attempted.Environment
CodeGraph 1.5.0, Windows 11, native filesystem (no WSL). Line numbers are against
mainat the time of writing.