Files
codegraph/src/mcp/version.ts
T
Colby MchenryandClaude Opus 5.5 dea076fd1e fix(mcp,extraction): exit a daemon whose install changed; never store a grammar-load failure (#2335, #2336) (#2346)
* fix(mcp,extraction): a daemon whose install changed exits; grammar-load failures are never stored (#2335, #2336)

A daemon that outlived an upgrade loaded grammars lazily from an install
that no longer existed, and stored every file its watcher re-indexed as a
zero-node row under the file's new content hash: no hash-based sync ever
revisited those rows, and `codegraph status` called the index up to date.

- The daemon checks its own package.json every 30 s
  (CODEGRAPH_DAEMON_INSTALL_CHECK_MS, 0 turns it off) and exits once the
  file is gone or carries another version, so the next session starts a
  daemon from the current install.
- A result whose grammar failed to load (`parser_error`) is never stored:
  the file keeps its previous data and the next sync or index retries it,
  and a row an older engine stored that way is re-indexed even though its
  hash matches. The tag-based CFML path reports a missing grammar with the
  same code.
- A launcher whose own install was deleted no longer crashes on the spawn
  error of the daemon it tried to start.
- `codegraph status` and `status --json` (filesNeedingReindex,
  filesWithParseErrors) report indexed files whose symbols are missing
  although their content is current (#2336), and `files --json` carries
  each file's recorded errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): a session whose own install changed never claims the writer lock (#2335)

A daemon now exits once its install is upgraded or removed, and nobody
holds the writer slot for a successor at that point. Its sessions run the
same replaced code and serve themselves in-process the moment it goes; one
that found the slot free claimed it as its writer even though its engine
could not load (a real npm upgrade: "Cannot find module
'./sqlite-adapter'"), and so kept a daemon from the current install from
starting. Such a session now falls back read-only and says to restart it.

Also adds the CHANGELOG entries for the install check and grammar-load
failures (#2335) and for the status report (#2336).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:08:14 +00:00

62 lines
2.4 KiB
TypeScript

/**
* Resolved package version, computed once at module load.
*
* The version string is the rendezvous datum between cooperating daemon and
* proxy processes: the daemon advertises its version in the hello line, and
* the proxy refuses to share IPC across a mismatch (falls back to direct
* mode), or replaces a daemon of an older release ({@link isOlderRelease}).
* Keeping the resolution in one place avoids drift between the CLI
* `--version` output (which reads `package.json` directly) and the daemon
* handshake.
*
* Resolution strategy: read the bundled `package.json` two levels up from
* this file — same relative position whether we're loaded from `src/mcp/` or
* the `dist/mcp/` output, since `tsc` preserves the layout. If reading fails
* (e.g. the package was unpacked oddly), fall back to "0.0.0-unknown" — a
* sentinel that will never match a real version, so the proxy harmlessly
* falls back to direct mode.
*/
import * as fs from 'fs';
import * as path from 'path';
/** The `package.json` of the install this process runs from. */
export const CodeGraphPackageJsonPath = path.join(__dirname, '..', '..', 'package.json');
function readPackageVersion(): string {
try {
const raw = fs.readFileSync(CodeGraphPackageJsonPath, 'utf8');
const parsed = JSON.parse(raw);
if (typeof parsed?.version === 'string' && parsed.version.length > 0) {
return parsed.version;
}
} catch {
// Fall through to sentinel.
}
return '0.0.0-unknown';
}
export const CodeGraphPackageVersion = readPackageVersion();
/**
* Whether `version` is a CodeGraph release older than `than` — the test a
* launcher applies before it replaces a running daemon (#2335). Only plain
* `MAJOR.MINOR.PATCH` releases compare: a prerelease, a build suffix or the
* "0.0.0-unknown" sentinel is never older. A daemon is replaced only by a
* strictly newer release, so two installed versions can never take turns
* stopping each other's daemon.
*/
export function isOlderRelease(version: string, than: string): boolean {
const a = parseRelease(version);
const b = parseRelease(than);
if (!a || !b) return false;
if (a[0] !== b[0]) return a[0] < b[0];
if (a[1] !== b[1]) return a[1] < b[1];
return a[2] < b[2];
}
function parseRelease(version: string): [number, number, number] | null {
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}