gitnexus 1.6.10-rc.221 → 1.6.10-rc.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,6 +122,15 @@ export declare const initLbug: (repoId: string, dbPath: string) => Promise<boole
122
122
  * repoId already injected it), the existing entry is reused.
123
123
  */
124
124
  export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
125
+ /**
126
+ * Lazily load VECTOR for a semantic query.
127
+ *
128
+ * Exact graph reads never call this function, so opening their read pool does
129
+ * not probe or warn about an optional extension they do not use. The promise
130
+ * lives on SharedDB because extension scope is per Database, and also joins
131
+ * concurrent first semantic requests onto one LOAD attempt.
132
+ */
133
+ export declare function ensureVectorExtension(repoId: string): Promise<boolean>;
125
134
  export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
126
135
  /**
127
136
  * Execute a parameterized query on a specific repo's connection pool.
@@ -265,6 +265,7 @@ function closeOne(repoId) {
265
265
  shared.refCount = 0;
266
266
  shared.ftsLoaded = false;
267
267
  shared.vectorLoaded = false;
268
+ shared.vectorLoadPromise = undefined;
268
269
  }
269
270
  else {
270
271
  shared.db.close().catch(() => { });
@@ -692,13 +693,6 @@ async function doInitLbug(repoId, dbPath) {
692
693
  if (!shared.ftsLoaded) {
693
694
  shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
694
695
  }
695
- // VECTOR too — extension load scope is per-Database, so this one load
696
- // makes QUERY_VECTOR_INDEX legal on every pooled connection. Same
697
- // load-only contract as FTS above; on failure the semantic-query lane
698
- // falls back to the exact scan with its own diagnostic (#2623 follow-up).
699
- if (!shared.vectorLoaded) {
700
- shared.vectorLoaded = await loadVectorExtension(available[0], { policy: 'load-only' });
701
- }
702
696
  // Register pool entry only after all connections are pre-warmed and FTS is
703
697
  // loaded. Concurrent executeQuery calls see either "not initialized"
704
698
  // (and throw cleanly) or a fully ready pool — never a half-built one.
@@ -761,11 +755,6 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
761
755
  if (!shared.ftsLoaded) {
762
756
  shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
763
757
  }
764
- // VECTOR too — same per-Database scope and load-only contract as the
765
- // doInitLbug site above (#2623 follow-up).
766
- if (!shared.vectorLoaded) {
767
- shared.vectorLoaded = await loadVectorExtension(available[0], { policy: 'load-only' });
768
- }
769
758
  pool.set(repoId, {
770
759
  db: existingDb,
771
760
  available,
@@ -780,6 +769,143 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
780
769
  ensureIdleTimer();
781
770
  traceRss('init', repoId);
782
771
  }
772
+ /**
773
+ * Lazily load VECTOR for a semantic query.
774
+ *
775
+ * Exact graph reads never call this function, so opening their read pool does
776
+ * not probe or warn about an optional extension they do not use. The promise
777
+ * lives on SharedDB because extension scope is per Database, and also joins
778
+ * concurrent first semantic requests onto one LOAD attempt.
779
+ */
780
+ export async function ensureVectorExtension(repoId) {
781
+ const entry = pool.get(repoId);
782
+ if (!entry) {
783
+ throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
784
+ }
785
+ const shared = dbCache.get(entry.dbPath);
786
+ if (!shared) {
787
+ throw new Error(`LadybugDB shared handle is unavailable for repo "${repoId}".`);
788
+ }
789
+ if (shared.vectorLoaded)
790
+ return true;
791
+ if (shared.vectorLoadPromise)
792
+ return shared.vectorLoadPromise;
793
+ const loadAttempt = (async () => {
794
+ const conn = await checkout(entry);
795
+ try {
796
+ const loaded = await loadVectorExtension(conn, { policy: 'load-only' });
797
+ shared.vectorLoaded = loaded;
798
+ return loaded;
799
+ }
800
+ finally {
801
+ checkin(entry, conn);
802
+ }
803
+ })();
804
+ const cachedAttempt = loadAttempt.catch((err) => {
805
+ // A transient checkout/load failure must not poison this Database for the
806
+ // rest of its lifetime. Keep resolved false cached, but let a later
807
+ // semantic request retry a rejected attempt.
808
+ if (shared.vectorLoadPromise === cachedAttempt) {
809
+ shared.vectorLoadPromise = undefined;
810
+ }
811
+ throw err;
812
+ });
813
+ shared.vectorLoadPromise = cachedAttempt;
814
+ return shared.vectorLoadPromise;
815
+ }
816
+ /**
817
+ * Detect an actual VECTOR procedure call without treating source text stored in
818
+ * Cypher literals or comments as executable syntax.
819
+ */
820
+ function callsVectorIndex(cypher) {
821
+ if (!/QUERY_VECTOR_INDEX/i.test(cypher))
822
+ return false;
823
+ let code = '';
824
+ let state = 'code';
825
+ let backtickIdentifier = '';
826
+ for (let i = 0; i < cypher.length; i++) {
827
+ const ch = cypher[i];
828
+ const next = cypher[i + 1];
829
+ if (state === 'code') {
830
+ if (ch === "'" || ch === '"' || ch === '`') {
831
+ state = ch === "'" ? 'single' : ch === '"' ? 'double' : 'backtick';
832
+ if (state === 'backtick')
833
+ backtickIdentifier = '';
834
+ code += ' ';
835
+ }
836
+ else if (ch === '/' && next === '/') {
837
+ state = 'line-comment';
838
+ code += ' ';
839
+ i++;
840
+ }
841
+ else if (ch === '/' && next === '*') {
842
+ state = 'block-comment';
843
+ code += ' ';
844
+ i++;
845
+ }
846
+ else {
847
+ code += ch;
848
+ }
849
+ continue;
850
+ }
851
+ if (state === 'line-comment') {
852
+ if (ch === '\n' || ch === '\r') {
853
+ state = 'code';
854
+ code += ch;
855
+ }
856
+ else {
857
+ code += ' ';
858
+ }
859
+ continue;
860
+ }
861
+ if (state === 'block-comment') {
862
+ if (ch === '*' && next === '/') {
863
+ state = 'code';
864
+ code += ' ';
865
+ i++;
866
+ }
867
+ else {
868
+ code += ch === '\n' || ch === '\r' ? ch : ' ';
869
+ }
870
+ continue;
871
+ }
872
+ if (state === 'backtick') {
873
+ if (ch === '`' && next === '`') {
874
+ backtickIdentifier += '`';
875
+ code += ' ';
876
+ i++;
877
+ }
878
+ else if (ch === '`') {
879
+ state = 'code';
880
+ code +=
881
+ backtickIdentifier.toUpperCase() === 'QUERY_VECTOR_INDEX' ? 'QUERY_VECTOR_INDEX' : ' ';
882
+ }
883
+ else if (ch === '\\' && next !== undefined) {
884
+ backtickIdentifier += next;
885
+ code += ' ';
886
+ i++;
887
+ }
888
+ else {
889
+ backtickIdentifier += ch;
890
+ code += ch === '\n' || ch === '\r' ? ch : ' ';
891
+ }
892
+ continue;
893
+ }
894
+ if (ch === '\\') {
895
+ code += ' ';
896
+ if (next !== undefined) {
897
+ code += next === '\n' || next === '\r' ? next : ' ';
898
+ i++;
899
+ }
900
+ continue;
901
+ }
902
+ const closesLiteral = (state === 'single' && ch === "'") || (state === 'double' && ch === '"');
903
+ if (closesLiteral)
904
+ state = 'code';
905
+ code += ch === '\n' || ch === '\r' ? ch : ' ';
906
+ }
907
+ return /\bCALL\s+QUERY_VECTOR_INDEX\s*\(/i.test(code);
908
+ }
783
909
  /**
784
910
  * Checkout a connection from the pool.
785
911
  * Returns an available connection, or creates a new one if under the cap.
@@ -870,12 +996,26 @@ export const executeParameterized = async (repoId, cypher, params) => {
870
996
  // a query built by splicing a caller-sized list names itself even when the
871
997
  // repo is not initialized. Never throws (#2915).
872
998
  warnIfQueryTextUnbounded(cypher, `pool executeParameterized (repo "${repoId}")`, (message) => poolSidecarLogger.warn(message));
873
- const entry = pool.get(repoId);
999
+ let entry = pool.get(repoId);
874
1000
  if (!entry) {
875
1001
  throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
876
1002
  }
877
- entry.lastUsed = Date.now();
1003
+ // Exact reads must not pay for VECTOR, but an explicit raw vector procedure
1004
+ // call is a semantic read. Preflight before taking the query connection:
1005
+ // ensureVectorExtension performs its own checkout, so holding one here could
1006
+ // make a saturated pool wait for a connection that every caller is holding.
1007
+ // A load rejection must not replace the query's own diagnostic.
1008
+ if (callsVectorIndex(cypher)) {
1009
+ await ensureVectorExtension(repoId).catch(() => false);
1010
+ // The preflight suspends, so close/re-init may replace the pool entry.
1011
+ // Re-read it before checkout to avoid querying through a stale handle.
1012
+ entry = pool.get(repoId);
1013
+ if (!entry) {
1014
+ throw new Error(`LadybugDB connection pool closed for repo "${repoId}" (re-init/teardown); retry the query.`);
1015
+ }
1016
+ }
878
1017
  const conn = await checkout(entry);
1018
+ entry.lastUsed = Date.now();
879
1019
  silenceStdout();
880
1020
  activeQueryCount++;
881
1021
  let queryResult;
@@ -256,12 +256,12 @@ export declare class LocalBackend {
256
256
  */
257
257
  private warnedSiblingDrift;
258
258
  /**
259
- * One-shot stderr warning for the VECTOR-extension fallback. Without this
260
- * guard the diagnostic would fire on every `semanticSearch()` call on
261
- * platforms where the extension is unsupported (e.g. Windows), making MCP
262
- * stderr noisy per DoD §2.8.
259
+ * One-shot stderr guards for distinct VECTOR load and index-query failures.
260
+ * Keeping them separate preserves both diagnostics across semanticSearch calls
261
+ * without repeating either on hot paths.
263
262
  */
264
- private warnedVectorUnsupported;
263
+ private warnedVectorLoadFailed;
264
+ private warnedVectorQueryFailed;
265
265
  /**
266
266
  * One-shot warning when a pruned or Node-unloadable optional embedding stack
267
267
  * (#2370/#2372) forces semantic search to fall back to BM25 — so the
@@ -8,7 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import { createHash } from 'crypto';
11
- import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
11
+ import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
12
12
  import { queryClassBeanMetadata } from './bean-metadata.js';
13
13
  import { querySpringAopMetadata } from './aop-metadata.js';
14
14
  import { queryConvexDispatchMetadata } from './convex-metadata.js';
@@ -783,12 +783,12 @@ export class LocalBackend {
783
783
  */
784
784
  warnedSiblingDrift = new Set();
785
785
  /**
786
- * One-shot stderr warning for the VECTOR-extension fallback. Without this
787
- * guard the diagnostic would fire on every `semanticSearch()` call on
788
- * platforms where the extension is unsupported (e.g. Windows), making MCP
789
- * stderr noisy per DoD §2.8.
786
+ * One-shot stderr guards for distinct VECTOR load and index-query failures.
787
+ * Keeping them separate preserves both diagnostics across semanticSearch calls
788
+ * without repeating either on hot paths.
790
789
  */
791
- warnedVectorUnsupported = false;
790
+ warnedVectorLoadFailed = false;
791
+ warnedVectorQueryFailed = false;
792
792
  /**
793
793
  * One-shot warning when a pruned or Node-unloadable optional embedding stack
794
794
  * (#2370/#2372) forces semantic search to fall back to BM25 — so the
@@ -2518,17 +2518,25 @@ export class LocalBackend {
2518
2518
  this.lastQueryEmbeddingDims.set(repo.lbugPath, dims);
2519
2519
  const queryVecStr = `[${queryVec.join(',')}]`;
2520
2520
  const maxDistance = getVectorMaxDistance(DEFAULT_MCP_VECTOR_MAX_DISTANCE);
2521
- let bestChunks = new Map();
2522
- // Always TRY the vector lane — no platform gate. LadybugDB ships the
2523
- // VECTOR extension for every supported platform, Windows included
2524
- // (#2623 follow-up; the old `platform !== 'win32'` gate was stale), so
2525
- // whether the index is queryable is a per-machine runtime fact. The
2526
- // catch below is the fallback: any failure (extension unloadable, index
2527
- // absent, older DB) degrades to the exact scan with a once-per-backend
2528
- // diagnostic instead of being silently swallowed.
2521
+ let vectorReady = false;
2529
2522
  try {
2530
- bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
2531
- const vectorQuery = `
2523
+ vectorReady = await ensureVectorExtension(repo.lbugPath);
2524
+ }
2525
+ catch (err) {
2526
+ if (!this.warnedVectorLoadFailed) {
2527
+ this.warnedVectorLoadFailed = true;
2528
+ logger.warn({ err }, 'GitNexus [query:vector]: vector extension load failed; using exact scan fallback');
2529
+ }
2530
+ }
2531
+ let bestChunks = new Map();
2532
+ // Try the vector lane only after its lazy load succeeds. An unavailable
2533
+ // extension is already reported by ExtensionManager; an index/query
2534
+ // failure below gets its own once-per-backend diagnostic before the exact
2535
+ // scan fallback.
2536
+ if (vectorReady) {
2537
+ try {
2538
+ bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
2539
+ const vectorQuery = `
2532
2540
  CALL QUERY_VECTOR_INDEX('${EMBEDDING_TABLE_NAME}', '${EMBEDDING_INDEX_NAME}',
2533
2541
  CAST(${queryVecStr} AS FLOAT[${dims}]), ${fetchLimit})
2534
2542
  YIELD node AS emb, distance
@@ -2538,24 +2546,25 @@ export class LocalBackend {
2538
2546
  emb.startLine AS startLine, emb.endLine AS endLine, distance
2539
2547
  ORDER BY distance
2540
2548
  `;
2541
- const embResults = await executeQuery(repo.lbugPath, vectorQuery);
2542
- return embResults.map((row) => ({
2543
- nodeId: row.nodeId ?? row[0],
2544
- chunkIndex: row.chunkIndex ?? row[1] ?? 0,
2545
- startLine: row.startLine ?? row[2] ?? 0,
2546
- endLine: row.endLine ?? row[3] ?? 0,
2547
- distance: row.distance ?? row[4],
2548
- }));
2549
- });
2550
- }
2551
- catch (err) {
2552
- bestChunks = new Map();
2553
- if (!this.warnedVectorUnsupported) {
2554
- // Rare diagnostic: surface why semantic search fell back to the
2555
- // exact scan. Emitted once per `LocalBackend` instance lifetime to
2556
- // avoid noisy stderr on hot semantic-search paths (DoD §2.8).
2557
- this.warnedVectorUnsupported = true;
2558
- logger.warn({ err }, 'GitNexus [query:vector]: vector index query failed; using exact scan fallback');
2549
+ const embResults = await executeQuery(repo.lbugPath, vectorQuery);
2550
+ return embResults.map((row) => ({
2551
+ nodeId: row.nodeId ?? row[0],
2552
+ chunkIndex: row.chunkIndex ?? row[1] ?? 0,
2553
+ startLine: row.startLine ?? row[2] ?? 0,
2554
+ endLine: row.endLine ?? row[3] ?? 0,
2555
+ distance: row.distance ?? row[4],
2556
+ }));
2557
+ });
2558
+ }
2559
+ catch (err) {
2560
+ bestChunks = new Map();
2561
+ if (!this.warnedVectorQueryFailed) {
2562
+ // Rare diagnostic: surface why semantic search fell back to the
2563
+ // exact scan. Emitted once per `LocalBackend` instance lifetime to
2564
+ // avoid noisy stderr on hot semantic-search paths (DoD §2.8).
2565
+ this.warnedVectorQueryFailed = true;
2566
+ logger.warn({ err }, 'GitNexus [query:vector]: vector index query failed; using exact scan fallback');
2567
+ }
2559
2568
  }
2560
2569
  }
2561
2570
  if (bestChunks.size === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.221",
3
+ "version": "1.6.10-rc.222",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",