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