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 @@ -26,6 +26,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- The shared background server no longer logs a scary-looking `[error] … undefined` line on every session start. Attaching to the shared daemon is normal, healthy behavior, but the informational message was being surfaced by MCP hosts (Claude Code and others) as an error; it's now silent by default — set `CODEGRAPH_MCP_LOG_ATTACH=1` to surface it when debugging daemon attach. Thanks @mturac. (#618)
- On Windows, CodeGraph's background processes no longer pile up without bound and saturate CPU over a long session. When the editor or agent that launched CodeGraph exited, its helper process couldn't tell its parent had gone — Windows reports process lineage differently than macOS and Linux — so the helper kept running, the shared background server never saw the client disconnect, and its idle timer never fired to shut it down. CodeGraph now detects parent-process exit directly on Windows, so helpers and the idle background server wind down promptly, the same as they already did on macOS and Linux. (#692, #576, #680)
- The shared background server has two further safeguards against ever lingering: it now drops a client the moment it detects that client's process is gone (even if the disconnect arrived uncleanly — a force-quit or a dropped connection that never closed the socket), and it won't stay running indefinitely with clients attached but no activity. Together these guarantee it always winds down, on every platform. (#692)
- A session no longer loses CodeGraph when the shared background server is restarted out from under it — for example when your MCP host (opencode and others) stops and restarts the server as you open another session. Previously the affected session's connection died silently and any request in flight at that moment hung; now CodeGraph keeps that session working by serving it locally, so the tools stay available without restarting the session. (#662)
Expand Down
38 changes: 38 additions & 0 deletions __tests__/daemon-attach-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* #618 — the "attached to shared daemon" line is benign INFO, but MCP hosts
* render server stderr at error level (and tack on an `undefined` data field),
* so on every session start a healthy attach showed up as `[error] … undefined`.
* It's now gated behind CODEGRAPH_MCP_LOG_ATTACH=1 — silent by default, opt-in
* for debugging. Approach from #640 by @mturac.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { logAttachedDaemon } from '../src/mcp/proxy';

const hello = { pid: 4242, codegraph: '9.9.9' } as any;

describe('daemon attach log gating (#618)', () => {
let spy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
spy = vi.spyOn(process.stderr, 'write').mockImplementation((() => true) as any);
});

afterEach(() => {
spy.mockRestore();
delete process.env.CODEGRAPH_MCP_LOG_ATTACH;
});

it('is silent by default (no [error]/undefined noise in MCP hosts)', () => {
delete process.env.CODEGRAPH_MCP_LOG_ATTACH;
logAttachedDaemon('/tmp/cg.sock', hello);
expect(spy).not.toHaveBeenCalled();
});

it('logs the attach line only when CODEGRAPH_MCP_LOG_ATTACH=1 (opt-in debug)', () => {
process.env.CODEGRAPH_MCP_LOG_ATTACH = '1';
logAttachedDaemon('/tmp/cg.sock', hello);
const out = spy.mock.calls.map((c) => String(c[0])).join('');
expect(out).toContain('Attached to shared daemon on /tmp/cg.sock');
expect(out).toContain('pid 4242');
});
});
5 changes: 4 additions & 1 deletion __tests__/mcp-daemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ function spawnServer(cwd: string, env: NodeJS.ProcessEnv = {}): SpawnedServer {
const child = spawn(process.execPath, [BIN, 'serve', '--mcp'], {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ...env },
// #618: the daemon-attach log line is now off by default; opt the test
// harness into it (CODEGRAPH_MCP_LOG_ATTACH=1) so the attach assertions
// below can still observe a successful attach. A per-test env still wins.
env: { CODEGRAPH_MCP_LOG_ATTACH: '1', ...process.env, ...env },
}) as ChildProcessWithoutNullStreams;
// Swallow spawn/EPIPE errors so killing a child mid-write can't surface as an
// unhandled error that crashes the vitest worker.
Expand Down
28 changes: 22 additions & 6 deletions src/mcp/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ import type { MCPEngine } from './engine';
/** Default poll cadence for the PPID watchdog (same as the direct server). */
const DEFAULT_PPID_POLL_MS = 5000;

/**
* Env var that opts INTO the "attached to shared daemon" log line. Off by
* default: the line is benign INFO, but MCP hosts render any server stderr at
* error level (and append an `undefined` data field), so on every session start
* a healthy attach showed up as `[error] … undefined`. Set to `1` to surface it
* when debugging daemon attach. (#618; approach from #640 by @mturac)
*/
const LOG_ATTACH_ENV = 'CODEGRAPH_MCP_LOG_ATTACH';

/**
* Log a successful daemon attach — gated behind {@link LOG_ATTACH_ENV} so it is
* silent by default (see #618). Exported for tests.
*/
export function logAttachedDaemon(socketPath: string, hello: DaemonHello): void {
if (process.env[LOG_ATTACH_ENV] !== '1') return;
process.stderr.write(
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
);
}

export interface ProxyResult {
/**
* `proxied` — successfully attached to a same-version daemon and piped
Expand Down Expand Up @@ -89,9 +109,7 @@ export async function runProxy(
return { outcome: 'fallback-needed', reason: 'version mismatch' };
}

process.stderr.write(
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
);
logAttachedDaemon(socketPath, hello);

sendClientHello(socket);
startPpidWatchdog(socket);
Expand Down Expand Up @@ -130,9 +148,7 @@ export async function connectWithHello(
socket.destroy();
return 'version-mismatch';
}
process.stderr.write(
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
);
logAttachedDaemon(socketPath, hello);
sendClientHello(socket);
return socket;
}
Expand Down