codegraph index told you to run `codegraph daemon stop`, which the CLI
rejects ("too many arguments"), whenever it could not verify the process
named in .codegraph/daemon.pid. In that state `codegraph daemon` lists
nothing, because it lists only daemons that answer their socket. And
`codegraph unlock` kept an old-format lock whose process was alive (the
rule from issue #1834), so nothing the CLI offered got past it. The
writer-lock message gave the same wrong advice.
Root cause: a live pid in the lock was the only evidence, and a pid
proves nothing once the OS gives it to another program. Every CodeGraph
daemon since the first one (v0.9.5) runs
`<runtime> ... codegraph.js serve --mcp --path <root>`, so the holder's
command line settles the question.
Fix:
- New src/mcp/process-identity.ts reads a process's command line and
executable name from the OS: Win32_Process through PowerShell (tasklist
as fallback) on Windows, /proc on Linux, ps elsewhere. Each lookup is
capped at 5 s and never throws. A holder is a CodeGraph MCP server,
provably something else, or unknown, and unknown is never evidence.
- stopDaemonAt treats an unverified live holder that is provably not a
CodeGraph server like a dead one. Its lock is cleared, under the same
writer-slot claim, and the process is never signalled.
clearStaleDaemonArtifacts (codegraph unlock) does the same for an
old-format lock. A holder that may be a daemon keeps its lock, as
before (issues #1553 and #1834).
- daemonStopFailureMessage builds index's error from what remains: the
pid, what it runs, and a command that clears it (taskkill /PID N /F or
kill N), or which lock file to delete when only that will do.
- writerLockHeldMessage names `codegraph daemon` instead of the missing
`codegraph daemon stop`.
Validation (Windows 11):
- New daemon-lock-holder.test.ts (11 tests) and a daemon-registry test.
With main's daemon-registry.ts, writer-lock.ts and codegraph.ts put
back, 11 tests fail, including all 8 CLI cases and the writer-lock
wording; with the fix all pass. Four existing tests used generic
processes to stand in for a daemon that doesn't answer. Those stand-ins
now carry a CodeGraph server's command line, and pass on main and here.
- Real CLI with a private HOME, 4 cases: an old-format lock naming a live
PING.EXE; an old-format lock naming a live Node process that isn't
CodeGraph; a current-format lock naming that kind of Node process with
a pipe nobody listens on; an old-format lock naming a Node process whose
command line reads `codegraph.js serve --mcp`. On main all 4 index runs
exit 1 with the old advice, `daemon stop` is rejected, `daemon` lists
nothing, and unlock clears only the third. With the fix the first 3
rebuild (exit 0), each holder stays alive, and unlock clears the first
too. The 4th exits 1, naming the pid and `taskkill /PID <pid> /F`;
after that command, index exits 0.
Contributor PR #2373 (@danusha2345) changes the two strings only; I read
it for guidance.
Fixes#2770
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live-holder case wrote `pid: 1` and leaned on its own comment, "On Linux,
PID 1 is almost always alive". Windows has no PID 1, so tryAcquireWriterLock
read the holder as dead and acquired — the correct behaviour for a dead
holder, and a failing assertion. The platform-specific part was the fixture,
not the lock.
A parked child process is alive on every platform, and not stealing from a
live holder is what the lock actually promises, so the case now tests the
promise rather than a number that happens to name a running process. The child
is reaped in afterEach.
Fixes#1752.
(cherry picked from commit bee140288d)
Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
* fix(mcp): keep an explicit projectPath project in sync (#1835)
A project opened through a tool call's `projectPath` (a repository other
than the server's default — e.g. an indexed child of an un-indexed
workspace) was opened read-only: no catch-up sync on open and no file
watcher, so its answers went stale until someone ran `codegraph sync`.
The engine now owns the lifecycle of every project the ToolHandler opens
for an explicit path, mirroring the default project:
- the project's writer lock is acquired (#1740 single-writer rule); if
another live process holds it, that process keeps syncing and we only
read;
- a catch-up `sync()` runs on open and the first call against that
project awaits it, time-boxed like the default gate (#905);
- a file watcher runs for as long as the project stays cached.
Bounds: the cache is keyed by the canonical (realpath) root, so a
symlinked spelling shares one connection and one watcher; it is LRU with
`MAX_CACHED_PROJECTS = 8` — opening a ninth closes the least recently
used (watcher stopped, writer lock released, DB closed); `stop()` closes
them all. Un-indexed paths still get the success-shaped guidance.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(mcp): keep explicit projects synchronized (#1835)
Explicit projectPath connections bypassed catch-up and synchronization ownership.
Share canonical project leases and catch-up gates, retain daemon sessions, and retry ownership after a writer exits.
Advertise direct-writer readiness and drain active work before eviction or shutdown.
Add exact SQLite regressions for concurrent sessions, aliases, no-watch behavior, and teardown.
Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(mcp): retry catch-up quietly on sync lock contention
Handle LockUnavailableError without marking catch-up complete or logging an alarming failure, and verify timer-driven recovery against a real indexing lock.
Make the eviction regression deterministic by applying its short timeout only to the held catch-up. Other projects must finish reconciling before the test can assert that the oldest eligible graph was closed.
---------
Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Concurrent CODEGRAPH_NO_DAEMON / in-process fallback serve --mcp instances
each started a FileWatcher and contended on codegraph.lock until auto-sync
degraded. Add an exclusive .codegraph/writer.pid lock held by the daemon or
the single direct writer; a second writer exits with actionable guidance.
Daemon mode still multiplexes N proxies onto one writer.
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>