Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph install` now sets up Pi: it adds the CodeGraph MCP server to `~/.pi/agent/mcp.json` or a project's `.pi/mcp.json`, plus a short CodeGraph section in `AGENTS.md`, and `codegraph_explore` stays on the model's tool list. Thanks @roymina.
- CodeGraph's MCP server now gives agents and editors its display name and icon, so one that shows them, like the Codex app, lists CodeGraph by name and logo instead of a plain `codegraph`. Thanks @fedosov.
- `codegraph status` now shows how many of a language's files yielded symbols when that isn't all of them, and warns when none did, so a parser that isn't working no longer looks healthy, and `codegraph status --strict` exits non-zero on that or on an index that's missing, unfinished or out of date, so packaging and CI can gate on it (#1658). Thanks @ferrine.
- Your agent now hears when a project's index predates the engine it's running on: after an upgrade, its first answer for that project warns once, naming the version that built the index and `codegraph index`, which stays your call since nothing rebuilds on its own, and `codegraph_status` always shows both versions. Thanks @cbeaulieu-gt. (#1852)
- `codegraph serve --mcp --no-telemetry` turns telemetry off for one MCP server entry, so a shared MCP config can opt out without an environment variable; a background daemon that's already running for the project keeps its own setting until it exits. Thanks @danusha2345. (#1908)
- `codegraph implementations <name>` lists the types that implement or extend a trait, interface or class, with each one's file and line, how it's tied to the type, and whether it's test code, a Rust mock in a `#[cfg(test)]` module included; `--depth` follows subclasses of subclasses. Thanks @danusha2345. (#2833)
- Set `CODEGRAPH_SYNTH_MIN_INTERVAL_MS` to limit how often live syncs rebuild the callback and event links across the whole project, which on a large TypeScript or Vue repo otherwise happens on almost every edit; a rebuild that's put off runs when the interval is up even if no edit follows, and until then those links can be that far behind your code. A full `codegraph index` is never held. Thanks @danusha2345. (#2911)
Expand Down
111 changes: 111 additions & 0 deletions __tests__/mcp-daemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { CodeGraph } from '../src';
import { getDaemonSocketPath } from '../src/mcp/daemon-paths';
import { CodeGraphPackageVersion } from '../src/mcp/version';
import { once } from 'events';
import { EXTRACTION_VERSION } from '../src/extraction/extraction-version';
import { WASM_RUNTIME_FLAGS } from '../src/extraction/wasm-runtime-flags';
import { SERVER_INFO } from '../src/mcp/session';
import { recordSpawns, removeSpawnLog, settleLosingCandidates } from './daemon-candidates';
Expand Down Expand Up @@ -169,6 +170,17 @@ function countListeningLines(root: string): number {
return readDaemonLog(root).split('\n').filter((l) => l.includes('[CodeGraph daemon] Listening on')).length;
}

function stampIndexStale(cg: CodeGraph, version: string): void {
const metadata = cg as unknown as {
queries: { setMetadata(key: string, value: string): void };
};
metadata.queries.setMetadata('indexed_with_version', version);
metadata.queries.setMetadata(
'indexed_with_extraction_version',
String(EXTRACTION_VERSION - 1),
);
}

function killTree(...procs: ChildProcessWithoutNullStreams[]): void {
for (const p of procs) {
if (!p.killed) { try { p.kill('SIGKILL'); } catch { /* gone */ } }
Expand Down Expand Up @@ -315,6 +327,105 @@ describe('Shared MCP daemon (issue #411)', () => {
expect(readLockPid(realRoot)).toBe(daemonPid);
}, 40000);

it('warns once per proxied client when the shared project index is stale (#1852)', async () => {
fs.writeFileSync(path.join(tempDir, 'alpha.ts'), 'export function alpha() { return 1; }\n');
const indexed = await CodeGraph.open(tempDir);
await indexed.indexAll();
stampIndexStale(indexed, '0.1.0');
indexed.close();

const env = { CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS: '15000' };
const first = spawnServer(tempDir, env);
const second = spawnServer(tempDir, env);
servers.push(first, second);
sendInitialize(first.child, `file://${tempDir}`, 1);
sendInitialize(second.child, `file://${tempDir}`, 1);
await waitFor(() => findResponse(first.stdout, 1), 10000);
await waitFor(() => findResponse(second.stdout, 1), 10000);
await waitFor(() => first.stderr.some((line) => line.includes('Attached to shared daemon')), 8000);
await waitFor(() => second.stderr.some((line) => line.includes('Attached to shared daemon')), 8000);

const callSearch = (server: SpawnedServer, id: number) => {
sendMessage(server.child, {
jsonrpc: '2.0',
id,
method: 'tools/call',
params: { name: 'codegraph_search', arguments: { query: 'alpha' } },
});
};
const responseText = (response: any): string => response.result.content[0].text;

callSearch(first, 2);
const firstInitial = await waitFor(() => findResponse(first.stdout, 2), 10000);

callSearch(first, 3);
callSearch(second, 2);
const firstRepeat = await waitFor(() => findResponse(first.stdout, 3), 10000);
const secondInitial = await waitFor(() => findResponse(second.stdout, 2), 10000);

expect(responseText(firstInitial)).toMatch(/index predates the running extraction engine/i);
expect(responseText(firstRepeat)).not.toMatch(/index predates/i);
expect(responseText(secondInitial)).toMatch(/index predates the running extraction engine/i);
expect(countListeningLines(realRoot)).toBe(1);
}, 45000);

it('does not repeat a daemon-delivered warning after proxy failover (#1852)', async () => {
fs.writeFileSync(path.join(tempDir, 'alpha.ts'), 'export function alpha() { return 1; }\n');
const indexed = await CodeGraph.open(tempDir);
await indexed.indexAll();
stampIndexStale(indexed, '0.1.0');
indexed.close();

const otherRoot = path.join(tempDir, 'other');
fs.mkdirSync(otherRoot);
fs.writeFileSync(path.join(otherRoot, 'bravo.ts'), 'export function bravo() { return 2; }\n');
const other = await CodeGraph.init(otherRoot, { index: false });
await other.indexAll();
stampIndexStale(other, '0.2.0');
other.close();

const env = { CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS: '30000', CODEGRAPH_PPID_POLL_MS: '5000' };
const server = spawnServer(tempDir, env);
servers.push(server);
sendInitialize(server.child, `file://${tempDir}`, 1);
await waitFor(() => findResponse(server.stdout, 1), 10000);
await waitFor(() => server.stderr.some((line) => line.includes('Attached to shared daemon')), 8000);
await waitFor(() => (readLockPid(realRoot) ?? 0) > 0, 8000);
const daemonPid = readLockPid(realRoot)!;

sendMessage(server.child, {
jsonrpc: '2.0', id: 2, method: 'tools/call',
params: { name: 'codegraph_search', arguments: { query: 'alpha' } },
});
const throughDaemon = await waitFor(() => findResponse(server.stdout, 2), 10000);
expect(throughDaemon.result.content[0].text).toMatch(/index predates/i);

process.kill(daemonPid, 'SIGTERM');
expect(await waitProcessExit(daemonPid, 8000)).toBe(true);
await waitFor(
() => server.stderr.some((line) => line.includes('serving this session in-process')),
8000,
);

sendMessage(server.child, {
jsonrpc: '2.0', id: 3, method: 'tools/call',
params: { name: 'codegraph_search', arguments: { query: 'alpha' } },
});
const afterFailover = await waitFor(() => findResponse(server.stdout, 3), 10000);
expect(afterFailover.result.content[0].text).not.toMatch(/index predates/i);

sendMessage(server.child, {
jsonrpc: '2.0', id: 4, method: 'tools/call',
params: {
name: 'codegraph_search',
arguments: { query: 'bravo', projectPath: otherRoot },
},
});
const otherProject = await waitFor(() => findResponse(server.stdout, 4), 10000);
expect(otherProject.result.content[0].text).toMatch(/index predates/i);
expect(otherProject.result.content[0].text).toContain('CodeGraph v0.2.0');
}, 45000);

it('concurrent launchers converge on a single daemon (lockfile race — must-fix 1)', async () => {
const env = { CODEGRAPH_DAEMON_IDLE_TIMEOUT_MS: '15000' };

Expand Down
Loading