( socketPath: string, expectedVersion: string = CodeGraphPackageVersion, )
| 129 | * owns the socket. Used by the local-handshake proxy's background connect. |
| 130 | */ |
| 131 | export async function connectWithHello( |
| 132 | socketPath: string, |
| 133 | expectedVersion: string = CodeGraphPackageVersion, |
| 134 | ): Promise<net.Socket | 'version-mismatch' | null> { |
| 135 | if (process.platform !== 'win32' && !fs.existsSync(socketPath)) return null; |
| 136 | const socket = net.createConnection(socketPath); |
| 137 | socket.setEncoding('utf8'); |
| 138 | // Keep an 'error' listener attached for the socket's ENTIRE life. readHelloLine |
| 139 | // attaches its own and then REMOVES it on success (its cleanup()), which left a |
| 140 | // window — from here until the caller attaches its onDaemonLost handler — where |
| 141 | // a socket 'error' had NO listener. In Node an unhandled socket 'error' is |
| 142 | // re-thrown as an uncaughtException, which the global fatal handler turns into |
| 143 | // process.exit(1); to an MCP client that surfaces as a bare "Transport closed" |
| 144 | // (#974). The window is rarely hit on a healthy FS but is common on flaky |
| 145 | // AF_UNIX-over-DrvFs (WSL2 /mnt drives). A no-op guard makes the error |
| 146 | // recoverable: the follow-up 'close' drives the caller's normal fallback. |
| 147 | socket.on('error', () => { /* absorbed — see #974; 'close' drives the fallback */ }); |
| 148 | const hello = await readHelloLine(socket).catch(() => null); |
| 149 | if (!hello) { |
| 150 | socket.destroy(); |
| 151 | return null; // no daemon yet — caller should keep polling |
| 152 | } |
| 153 | if (hello.codegraph !== expectedVersion) { |
| 154 | // A daemon IS up but it's the wrong version — definitive, not a "not yet". |
| 155 | // Don't poll; the caller serves in-process so we never run stale-vs-new. |
| 156 | process.stderr.write( |
| 157 | `[CodeGraph MCP] Found a daemon on ${socketPath} but version (${hello.codegraph}) ` + |
| 158 | `differs from ours (${expectedVersion}); serving this session in-process.\n` |
| 159 | ); |
| 160 | socket.destroy(); |
| 161 | return 'version-mismatch'; |
| 162 | } |
| 163 | logAttachedDaemon(socketPath, hello); |
| 164 | sendClientHello(socket); |
| 165 | return socket; |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Tell the daemon our pids right after we verify its hello, so its liveness |
no test coverage detected