From ace8d8a0d0597d61ee07472371b35ca87f5714c7 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 22 Jun 2026 11:31:49 -0500 Subject: [PATCH] fix(mcp): stop the first tool call hanging on a huge-repo catch-up reconcile (#905) (#950) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a very large repo (the report is a ~93k-file / 5.7GB-DB Java monorepo) the first MCP `tools/call` after a fresh `serve --mcp` could hang for 10+ minutes with zero output, and with the liveness watchdog on, the daemon was SIGKILLed mid-query instead. Root cause: the post-open catch-up reconcile that the first tool call is gated on does ~2*N synchronous `fs.existsSync`/`fs.statSync` calls plus a load-all-files query in two non-yielding loops. On a huge repo that wedges the event loop for minutes, which (a) trips the 60s watchdog (it SIGKILLs a process whose loop stops turning) and (b) blocks the first call the whole time. Two complementary fixes: - Make the reconcile yield. `ExtractionOrchestrator.sync()` now uses the yielding `scanDirectoryAsync`, and both O(files) reconcile loops `await setImmediate` every SYNC_RECONCILE_YIELD_INTERVAL (1000) files. The loop can no longer wedge the main thread, so the watchdog stays fed and the socket / any concurrent read stays responsive while a big reconcile runs. Results are unchanged — only yield points are added. - Time-box the catch-up gate. The first `tools/call` now waits on the reconcile for at most CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS (default 3000ms), then serves and lets the reconcile finish in the background (which now yields, so the served call runs concurrently). `=0` restores the old unbounded wait. On a normal repo the reconcile finishes well under the budget, so behavior is unchanged. Tests: adds two time-box cases to mcp-catchup-gate (serves promptly when the reconcile runs long; `=0` restores the unbounded wait). Full suite green (1655 passed). Validated end-to-end through the real daemon: first call returns at the ~3s time-box instead of waiting an injected 8s reconcile; no-delay control unchanged; `=0` opt-out waits the full reconcile. Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + __tests__/mcp-catchup-gate.test.ts | 51 +++++++++++++++++++++ src/extraction/index.ts | 26 ++++++++++- src/mcp/tools.ts | 73 ++++++++++++++++++++++++++++-- 4 files changed, 145 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d93308f..64d20a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- An MCP server pointed at a very large repository (tens of thousands of files) no longer hangs on the first tool call after a fresh start. On startup CodeGraph reconciles its index against the current files on disk, and on a huge repo that reconcile could run for minutes while blocking the very first request — long enough that the background server was sometimes force-restarted mid-scan, so the first query never came back at all. The reconcile now yields as it runs (keeping the server responsive instead of pinning it), and the first tool call waits only briefly for it before answering and letting the rest finish in the background — so you get a fast first response and the index still catches up. Set `CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS` to tune how long that first call waits (default 3000ms), or `=0` to always wait for the full reconcile. (#905) - `codegraph install` now wires up your agents and stops there — it no longer indexes the current directory. Building a project's graph is always the explicit `codegraph init` (or `codegraph index`), so you decide what gets indexed and when, and the steps are the same whether you installed globally or just for one project. This clears up the confusion where a project-local install silently indexed but a global one didn't, and where the docs and the tool disagreed about whether you still had to run `init`. (#826) - React components declared with `forwardRef`, `memo`, or styled-components / emotion (`const Button = forwardRef(...)`, `const Card = memo(...)`, `const Box = styled.button\`…\``) are now recognized as components, so finding where they're used works. Before, they were indexed as plain constants, so `codegraph callers` and impact analysis reported "no callers found" even when the component was rendered across dozens of files — a dangerous false "safe to change" right before refactoring a shared component. Now every `