From d4d0132b13ff2a600e98137415ee2c54f50fea8a Mon Sep 17 00:00:00 2001 From: skyvanguard Date: Sun, 15 Mar 2026 17:16:00 -0300 Subject: [PATCH] fix(mcp): detect stale DB after re-indexing and reconnect (#297) ensureInitialized() now reads meta.json on each tool call to compare indexedAt against the stored loadedAt timestamp. When gitnexus analyze rebuilds the index, the next query closes the stale LadybugDB pool entry and re-opens a fresh connection automatically. Co-Authored-By: Claude Opus 4.6 --- gitnexus/src/mcp/local/local-backend.ts | 25 ++++++++++++++++++- .../test/integration/augmentation.test.ts | 1 + .../local-backend-calltool.test.ts | 1 + gitnexus/test/unit/calltool-dispatch.test.ts | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 957be8eab..5408c89f5 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -16,6 +16,7 @@ import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady } import { listRegisteredRepos, cleanupOldKuzuFiles, + loadMeta, type RegistryEntry, } from '../../storage/repo-manager.js'; // AI context generation is CLI-only (gitnexus analyze) @@ -82,6 +83,7 @@ interface RepoHandle { indexedAt: string; lastCommit: string; stats?: RegistryEntry['stats']; + loadedAt?: string; // meta.indexedAt when DB was opened — for staleness detection } export class LocalBackend { @@ -247,7 +249,24 @@ export class LocalBackend { private async ensureInitialized(repoId: string): Promise { // Always check the actual pool — the idle timer may have evicted the connection - if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) return; + if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) { + // Staleness check: did `gitnexus analyze` rebuild the DB since we opened it? + const handle = this.repos.get(repoId); + if (handle?.loadedAt) { + const meta = await loadMeta(handle.storagePath); + if (meta?.indexedAt && meta.indexedAt !== handle.loadedAt) { + // DB was rebuilt — close stale connection and fall through to re-init + await closeLbug(repoId); + this.initializedRepos.delete(repoId); + this.contextCache.delete(repoId); + handle.loadedAt = undefined; + } else { + return; // Still fresh + } + } else { + return; // No loadedAt tracked yet (first run) + } + } const handle = this.repos.get(repoId); if (!handle) throw new Error(`Unknown repo: ${repoId}`); @@ -255,6 +274,10 @@ export class LocalBackend { try { await initLbug(repoId, handle.lbugPath); this.initializedRepos.add(repoId); + + // Record when we loaded so we can detect staleness later + const meta = await loadMeta(handle.storagePath); + handle.loadedAt = meta?.indexedAt; } catch (err: any) { // If lock error, mark as not initialized so next call retries this.initializedRepos.delete(repoId); diff --git a/gitnexus/test/integration/augmentation.test.ts b/gitnexus/test/integration/augmentation.test.ts index 339cb3392..d5256a126 100644 --- a/gitnexus/test/integration/augmentation.test.ts +++ b/gitnexus/test/integration/augmentation.test.ts @@ -54,6 +54,7 @@ const AUGMENT_FTS_INDEXES = [ // Mock repo-manager so augment() finds our test DB vi.mock('../../src/storage/repo-manager.js', () => ({ listRegisteredRepos: vi.fn(), + loadMeta: vi.fn().mockResolvedValue(null), })); let augment: (pattern: string, cwd?: string) => Promise; diff --git a/gitnexus/test/integration/local-backend-calltool.test.ts b/gitnexus/test/integration/local-backend-calltool.test.ts index e0b61bb62..fad411422 100644 --- a/gitnexus/test/integration/local-backend-calltool.test.ts +++ b/gitnexus/test/integration/local-backend-calltool.test.ts @@ -14,6 +14,7 @@ import { LOCAL_BACKEND_SEED_DATA, LOCAL_BACKEND_FTS_INDEXES } from '../fixtures/ vi.mock('../../src/storage/repo-manager.js', () => ({ listRegisteredRepos: vi.fn().mockResolvedValue([]), cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }), + loadMeta: vi.fn().mockResolvedValue(null), })); // ─── Block 2: callTool dispatch tests ──────────────────────────────── diff --git a/gitnexus/test/unit/calltool-dispatch.test.ts b/gitnexus/test/unit/calltool-dispatch.test.ts index 50263f356..9f4344240 100644 --- a/gitnexus/test/unit/calltool-dispatch.test.ts +++ b/gitnexus/test/unit/calltool-dispatch.test.ts @@ -21,6 +21,7 @@ vi.mock('../../src/mcp/core/lbug-adapter.js', () => ({ vi.mock('../../src/storage/repo-manager.js', () => ({ listRegisteredRepos: vi.fn().mockResolvedValue([]), cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }), + loadMeta: vi.fn().mockResolvedValue(null), })); // Also mock the search modules to avoid loading onnxruntime