diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a8f4018..9f7973ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/__tests__/daemon-attach-log.test.ts b/__tests__/daemon-attach-log.test.ts new file mode 100644 index 00000000..7640ac68 --- /dev/null +++ b/__tests__/daemon-attach-log.test.ts @@ -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; + + 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'); + }); +}); diff --git a/__tests__/mcp-daemon.test.ts b/__tests__/mcp-daemon.test.ts index 0f4abf94..c00d528f 100644 --- a/__tests__/mcp-daemon.test.ts +++ b/__tests__/mcp-daemon.test.ts @@ -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. diff --git a/src/mcp/proxy.ts b/src/mcp/proxy.ts index ab937306..d1864967 100644 --- a/src/mcp/proxy.ts +++ b/src/mcp/proxy.ts @@ -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 @@ -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); @@ -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; }