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:
+43
-40
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user