From 999fbf5b112ca3ef73292d7e80ae34d5fd3c1567 Mon Sep 17 00:00:00 2001 From: hiromima Date: Fri, 20 Mar 2026 13:33:20 +0900 Subject: [PATCH] fix: sequential enrichment queries + stale data detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes three related issues that cause SIGSEGV crashes and stale data: 1. Impact enrichment queries (Promise.all → sequential await) The impact() method ran 3 enrichment queries concurrently via Promise.all against the same LadybugDB connection pool. On arm64 macOS, concurrent native DB access triggers SIGSEGV. Changed to sequential await. Also caps IN-clause to 100 IDs to prevent oversized queries. (#285, #290, #292) 2. Silence stdout during query execution silenceStdout()/restoreStdout() only wrapped createConnection() and initLbug(). Now also wraps executeQuery() and executeParameterized() to prevent native stdout writes from corrupting the MCP stdio stream during all DB operations. (#285) 3. Stale data after re-index ensureInitialized() checked pool existence but never verified whether the underlying index was rebuilt. Now reads meta.json's indexedAt timestamp on each call and closes/re-opens the pool when the index has changed. (#297) --- gitnexus/src/mcp/core/lbug-adapter.ts | 4 ++ gitnexus/src/mcp/local/local-backend.ts | 77 ++++++++++++++++--------- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/gitnexus/src/mcp/core/lbug-adapter.ts b/gitnexus/src/mcp/core/lbug-adapter.ts index cf9bb1ad6..888e18b56 100644 --- a/gitnexus/src/mcp/core/lbug-adapter.ts +++ b/gitnexus/src/mcp/core/lbug-adapter.ts @@ -458,12 +458,14 @@ export const executeQuery = async (repoId: string, cypher: string): Promise { - // Always check the actual pool — the idle timer may have evicted the connection - if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) return; - const handle = this.repos.get(repoId); if (!handle) throw new Error(`Unknown repo: ${repoId}`); + // Check if the index was rebuilt since we opened the connection (#297). + // Read meta.json's indexedAt and compare to the cached value — if it + // changed, close the stale pool and re-initialize with the fresh index. + if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) { + try { + const metaPath = path.join(handle.storagePath, 'meta.json'); + const metaRaw = await fs.readFile(metaPath, 'utf-8'); + const meta = JSON.parse(metaRaw); + if (meta.indexedAt && meta.indexedAt !== handle.indexedAt) { + // Index was rebuilt — close stale connection and re-init + await closeLbug(repoId); + this.initializedRepos.delete(repoId); + handle.indexedAt = meta.indexedAt; + } else { + return; // Pool is current + } + } catch { + return; // Can't read meta — assume pool is fine + } + } + try { await initLbug(repoId, handle.lbugPath); this.initializedRepos.add(repoId); @@ -1438,31 +1456,36 @@ export class LocalBackend { let affectedModules: any[] = []; if (impacted.length > 0) { - const allIds = impacted.map(i => `'${i.id.replace(/'/g, "''")}'`).join(', '); - const d1Ids = (grouped[1] || []).map((i: any) => `'${i.id.replace(/'/g, "''")}'`).join(', '); + // Cap IN-clause to 100 IDs to prevent oversized queries that crash + // the native DB engine on arm64 macOS (#292) + const cappedImpacted = impacted.slice(0, 100); + const allIds = cappedImpacted.map(i => `'${i.id.replace(/'/g, "''")}'`).join(', '); + const d1Items = (grouped[1] || []).slice(0, 100); + const d1Ids = d1Items.map((i: any) => `'${i.id.replace(/'/g, "''")}'`).join(', '); - // Affected processes: which execution flows are broken and at which step - const [processRows, moduleRows, directModuleRows] = await Promise.all([ - executeQuery(repo.id, ` - MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) - WHERE s.id IN [${allIds}] - RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount - ORDER BY hits DESC - LIMIT 20 - `).catch(() => []), - executeQuery(repo.id, ` - MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) - WHERE s.id IN [${allIds}] - RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits - ORDER BY hits DESC - LIMIT 20 - `).catch(() => []), - d1Ids ? executeQuery(repo.id, ` - MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) - WHERE s.id IN [${d1Ids}] - RETURN DISTINCT c.heuristicLabel AS name - `).catch(() => []) : Promise.resolve([]), - ]); + // Run enrichment queries sequentially to avoid concurrent native DB + // access that causes SIGSEGV on arm64 macOS (#285, #290, #292) + const processRows = await executeQuery(repo.id, ` + MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) + WHERE s.id IN [${allIds}] + RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount + ORDER BY hits DESC + LIMIT 20 + `).catch(() => []); + const moduleRows = await executeQuery(repo.id, ` + MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + WHERE s.id IN [${allIds}] + RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits + ORDER BY hits DESC + LIMIT 20 + `).catch(() => []); + const directModuleRows = d1Ids + ? await executeQuery(repo.id, ` + MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + WHERE s.id IN [${d1Ids}] + RETURN DISTINCT c.heuristicLabel AS name + `).catch(() => []) + : []; affectedProcesses = processRows.map((r: any) => ({ name: r.name || r[0],