feat(mcp): trace relevance + closure-collection + god-file rendering + cold-start handshake (#580)

Trace endpoint relevance (overloaded names resolve to the real implementation instead of an empty protocol/delegate stub), Swift closure-collection synthesizer, multi-phase god-file explore rendering, and serve --mcp cold-start handshake sped ~811ms→~90ms (proxy answers initialize/tools-list locally). Full suite green (1090 pass).
This commit is contained in:
Colby Mchenry
2026-05-31 18:41:41 -05:00
committed by GitHub
parent b026e64b41
commit 3a1ddf41cd
12 changed files with 756 additions and 92 deletions
+43 -40
View File
@@ -37,8 +37,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { spawn, StdioOptions } from 'child_process';
import { findNearestCodeGraphRoot } from '../index';
import { getCodeGraphDir } from '../directory';
import { findNearestCodeGraphRoot, getCodeGraphDir } from '../directory';
import { StdioTransport } from './transport';
import { MCPEngine } from './engine';
import { MCPSession } from './session';
@@ -48,7 +47,7 @@ import {
isProcessAlive,
tryAcquireDaemonLock,
} from './daemon';
import { runProxy } from './proxy';
import { connectWithHello, runLocalHandshakeProxy } from './proxy';
import { getDaemonSocketPath } from './daemon-paths';
import { HOST_PPID_ENV } from '../extraction/wasm-runtime-flags';
@@ -82,8 +81,14 @@ const TAKEOVER_RETRY_DELAY_MS = 100;
* process startup. 60 × 100ms = 6s of headroom for a cold/slow box; on the
* common path the socket appears within a few rounds.
*/
const DAEMON_CONNECT_MAX_RETRIES = 60;
const DAEMON_CONNECT_RETRY_DELAY_MS = 100;
// Poll finely (25ms) so the proxy attaches the instant the freshly-spawned
// daemon binds, instead of waiting up to a coarse 100ms after — shaves the
// cold-start handshake (the window the headless agent races). Same ~6s total
// give-up budget (240 × 25ms), just finer granularity; socket-connect probes
// are cheap. Paired with deferring the CodeGraph load (engine.ts) off the bind
// path, this narrows the "No such tool available" race window.
const DAEMON_CONNECT_MAX_RETRIES = 240;
const DAEMON_CONNECT_RETRY_DELAY_MS = 25;
/**
* Resolve the PPID watchdog poll interval from an env override. A value of
@@ -258,21 +263,20 @@ export class MCPServer {
}
try {
const mode = await this.connectOrSpawnDaemon(root);
if (mode === 'fallback') {
return this.startDirect('daemon unavailable; fallback to direct');
}
// 'proxy': connectOrSpawnDaemon ran the stdio↔socket pipe to completion
// (it only returns once the host disconnected). The process is now
// expected to terminate naturally — the proxy installed its own watchdog.
// Answer the MCP handshake LOCALLY (instant tool registration — no waiting
// ~600ms for the daemon to spawn+bind, which produced the cold-start race)
// and forward tool CALLS to the shared daemon, connected in the background.
// Runs until the host disconnects; the proxy installs its own watchdog and
// falls back to an in-process engine if the daemon never comes up.
this.mode = 'proxy';
await this.runProxyWithLocalHandshake(root);
return;
} catch (err) {
// Belt-and-braces: if anything throws inside the daemon machinery,
// never wedge the user — fall back to a working direct-mode session.
// Belt-and-braces: a throw during proxy SETUP (before the client was served)
// is still safe to recover from with a direct-mode session.
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(`[CodeGraph MCP] Daemon path failed (${msg}); falling back to direct mode.\n`);
return this.startDirect('daemon path threw');
process.stderr.write(`[CodeGraph MCP] Proxy path failed (${msg}); falling back to direct mode.\n`);
return this.startDirect('proxy path threw');
}
}
@@ -376,32 +380,31 @@ export class MCPServer {
}
/**
* Become a proxy to the shared daemon, spawning the daemon first if none is
* reachable. Returns 'proxy' once the proxied session has run to completion
* (the host disconnected), or 'fallback' if the caller should run in-process.
* Proxy mode (the common case). Serve the MCP handshake LOCALLY for instant
* tool registration, forwarding tool calls to the shared daemon — which is
* connected in the background (probed, then spawned + polled if absent) so the
* handshake never waits ~600ms on it. Runs until the host disconnects; the
* proxy falls back to an in-process engine if the daemon never binds, so this
* never wedges a session.
*/
private async connectOrSpawnDaemon(root: string): Promise<'proxy' | 'fallback'> {
private async runProxyWithLocalHandshake(root: string): Promise<void> {
const socketPath = getDaemonSocketPath(root);
// Fast path: a daemon may already be listening. On success runProxy pipes
// stdio until the host disconnects, so a 'proxied' outcome means this
// process has finished its entire job.
let probe = await runProxy(socketPath);
if (probe.outcome === 'proxied') return 'proxy';
if (probe.reason === 'version mismatch') return 'fallback';
// No reachable daemon — spawn one (detached) and wait for it to bind.
spawnDetachedDaemon(root);
for (let attempt = 0; attempt < DAEMON_CONNECT_MAX_RETRIES; attempt++) {
await sleep(DAEMON_CONNECT_RETRY_DELAY_MS);
probe = await runProxy(socketPath);
if (probe.outcome === 'proxied') return 'proxy';
if (probe.reason === 'version mismatch') return 'fallback';
}
// Daemon never came up in time — run in-process so the user is never blocked.
return 'fallback';
const getDaemonSocket = async () => {
// Fast path: a daemon may already be listening.
const probe = await connectWithHello(socketPath);
if (probe === 'version-mismatch') return null; // definitive — serve in-process, don't poll for 6s
if (probe) return probe;
// None reachable — spawn one (detached) and poll for its bind.
spawnDetachedDaemon(root);
for (let attempt = 0; attempt < DAEMON_CONNECT_MAX_RETRIES; attempt++) {
await sleep(DAEMON_CONNECT_RETRY_DELAY_MS);
const s = await connectWithHello(socketPath);
if (s === 'version-mismatch') return null;
if (s) return s;
}
return null; // never bound — the proxy serves this session in-process
};
await runLocalHandshakeProxy({ getDaemonSocket, makeEngine: () => new MCPEngine(), root });
}
/** Standard SIGINT/SIGTERM handlers that route to our `stop()` (direct mode). */