gitnexus 1.6.8-rc.2 → 1.6.8-rc.4

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.
package/README.md CHANGED
@@ -436,6 +436,26 @@ After scope resolution, analyze prunes inert block-local value symbols (a functi
436
436
 
437
437
  Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
438
438
 
439
+ ### Hook augmentation/notifications are silently skipped
440
+
441
+ The Claude Code / Antigravity hooks intentionally stay **silent** on normal skip
442
+ paths so strict hook runners (e.g. Codex `PreToolUse`) never see unexpected
443
+ output. A search may not be augmented — or a stale-index reminder may not appear
444
+ on stderr — when the GitNexus MCP server owns the repo DB, when the DB-lock probe
445
+ times out and fails closed, or when the index is already current.
446
+
447
+ To see why a hook skipped, set `GITNEXUS_DEBUG=1` and re-run the action — the hook
448
+ writes the reason (e.g. `[GitNexus] augment skipped: MCP server owns DB`) and the
449
+ stale-index hint to its stderr:
450
+
451
+ ```bash
452
+ GITNEXUS_DEBUG=1 <your command> # surfaces hook skip/diagnostic reasons on stderr
453
+ ```
454
+
455
+ Only `GITNEXUS_DEBUG=1` and `GITNEXUS_DEBUG=true` enable diagnostics; every other
456
+ value (including `0` and `false`) is treated as off. Diagnostics go to stderr
457
+ only — the hook's structured stdout (the JSON the agent consumes) is unaffected.
458
+
439
459
  ## Privacy
440
460
 
441
461
  - All processing happens locally on your machine
@@ -16,8 +16,8 @@ import { extractStructuralNames } from './structural-extractor.js';
16
16
  import { EMBEDDABLE_LABELS, isShortLabel, LABEL_METHOD, LABELS_WITH_EXPORTED, STRUCTURAL_LABELS, collectBestChunks, } from './types.js';
17
17
  import { resolveEmbeddingConfig } from './config.js';
18
18
  import { rankExactEmbeddingRows } from './exact-search.js';
19
- import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from '../lbug/schema.js';
20
- import { loadVectorExtension } from '../lbug/lbug-adapter.js';
19
+ import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME, STALE_HASH_SENTINEL } from '../lbug/schema.js';
20
+ import { loadVectorExtension, createVectorIndex } from '../lbug/lbug-adapter.js';
21
21
  import { getExactScanLimit } from '../platform/capabilities.js';
22
22
  import { logger } from '../logger.js';
23
23
  const isDev = process.env.NODE_ENV === 'development';
@@ -153,24 +153,35 @@ export const batchInsertEmbeddings = async (executeWithReusedStatement, updates)
153
153
  await executeWithReusedStatement(cypher, paramsList);
154
154
  };
155
155
  /**
156
- * Create the vector index for semantic search
157
-
158
- * Now indexes the separate CodeEmbedding table.
159
- * Delegates extension loading to lbug-adapter's loadVectorExtension(),
160
- * which owns the VECTOR extension lifecycle and state tracking.
161
-
156
+ * Create the vector index for semantic search (indexes the CodeEmbedding table).
157
+ *
158
+ * Keeps the embedding-specific extension-install policy gate here
159
+ * (ensureVectorExtensionAvailable resolveEmbeddingInstallPolicy, default
160
+ * `auto` for the analyze write path), then delegates the actual
161
+ * `CALL CREATE_VECTOR_INDEX(...)` to the adapter, which runs it through the
162
+ * unprepared `conn.query()` path. It must NOT go through the injected
163
+ * `executeQuery` (prepared `conn.prepare()`): LadybugDB cannot prepare that
164
+ * procedure and fails with "We do not support prepare multiple statements" —
165
+ * the silent degrade in #2114.
162
166
  */
163
- const createVectorIndex = async (executeQuery) => {
167
+ const buildVectorIndex = async () => {
168
+ // This pre-check applies the embedding-specific install policy
169
+ // (resolveEmbeddingInstallPolicy, default `auto` for analyze) before reaching
170
+ // the adapter. The adapter's createVectorIndex() calls loadVectorExtension()
171
+ // again, but that's a no-op here: once this gate loads VECTOR the module-level
172
+ // `vectorExtensionLoaded` flag is set, so the adapter's second call
173
+ // short-circuits without re-resolving the policy — no double install.
164
174
  if (!(await ensureVectorExtensionAvailable()))
165
175
  return false;
166
176
  try {
167
- await executeQuery(CREATE_VECTOR_INDEX_QUERY);
168
- return true;
177
+ return await createVectorIndex();
169
178
  }
170
179
  catch (error) {
171
- if (isDev) {
172
- logger.warn({ error }, 'Vector index creation warning:');
173
- }
180
+ // Surface this even outside dev: it silently downgrades a user-requested
181
+ // feature (semantic search) to exact scan. Log under `err` so pino's
182
+ // standard serializer captures the message/stack — logging under `error`
183
+ // serialized an Error to `{}` (the empty `{"error":{}}` reported in #2114).
184
+ logger.warn({ err: error }, 'Vector index creation failed; semantic search will use exact-scan fallback');
174
185
  return false;
175
186
  }
176
187
  };
@@ -283,7 +294,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
283
294
  // Ensure the vector index exists even when no new nodes need embedding.
284
295
  // A prior crash or first-time incremental run may have left CodeEmbedding
285
296
  // rows without ever reaching index creation.
286
- const vectorIndexReady = await createVectorIndex(executeQuery);
297
+ const vectorIndexReady = await buildVectorIndex();
287
298
  onProgress({
288
299
  phase: 'ready',
289
300
  percent: 100,
@@ -403,7 +414,7 @@ export const runEmbeddingPipeline = async (executeQuery, executeWithReusedStatem
403
414
  if (isDev) {
404
415
  logger.info('📇 Creating vector index...');
405
416
  }
406
- const vectorIndexReady = await createVectorIndex(executeQuery);
417
+ const vectorIndexReady = await buildVectorIndex();
407
418
  onProgress({
408
419
  phase: 'ready',
409
420
  percent: 100,
@@ -233,6 +233,25 @@ export declare const loadVectorExtension: (targetConn?: lbug.Connection, opts?:
233
233
  * @param stemmer - Stemming algorithm (default: 'porter')
234
234
  */
235
235
  export declare const createFTSIndex: (tableName: string, indexName: string, properties: string[], stemmer?: string) => Promise<void>;
236
+ /**
237
+ * Create the HNSW vector index on the CodeEmbedding table.
238
+ *
239
+ * MUST run via `conn.query()` (here through `queryAndDrain`), NOT through the
240
+ * prepared `executeQuery`/`conn.prepare()` path: `CALL CREATE_VECTOR_INDEX(...)`
241
+ * compiles to multiple statements, which LadybugDB cannot prepare — it fails
242
+ * with "Connection Exception: We do not support prepare multiple statements."
243
+ * Routing index creation through `executeQuery` (prepared) is exactly what
244
+ * broke vector-index creation during `analyze` (#2114; the singleton
245
+ * `executeQuery` was switched to the prepared path in #1655 while FTS index
246
+ * creation kept using `conn.query()`, which is why FTS survived and VECTOR did
247
+ * not). Mirrors `createFTSIndex` above.
248
+ *
249
+ * Returns `true` on success (or when the index already exists — idempotent so
250
+ * incremental re-runs don't spuriously downgrade to exact scan), `false` when
251
+ * the VECTOR extension is unavailable or the connection is read-only. Any other
252
+ * failure propagates so the caller can log it.
253
+ */
254
+ export declare const createVectorIndex: () => Promise<boolean>;
236
255
  /**
237
256
  * Lazy-create an FTS index, caching the fact in-process.
238
257
  *
@@ -8,7 +8,7 @@ import os from 'os';
8
8
  import crypto from 'crypto';
9
9
  import lbug from '@ladybugdb/core';
10
10
  import { closeQueryResults } from './query-result-utils.js';
11
- import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
11
+ import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from './schema.js';
12
12
  import { streamAllCSVsToDisk } from './csv-generator.js';
13
13
  import { extensionManager } from './extension-loader.js';
14
14
  import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
@@ -119,6 +119,11 @@ let currentDbPath = null;
119
119
  let currentDbReadOnly = false;
120
120
  let ftsLoaded = false;
121
121
  let vectorExtensionLoaded = false;
122
+ // In-process guard so a repeated createVectorIndex() within one connection
123
+ // lifetime skips the DB round-trip (mirrors ensuredFTSIndexes). Reset wherever
124
+ // vectorExtensionLoaded resets, so it can never stay true against a swapped or
125
+ // closed connection.
126
+ let vectorIndexEnsured = false;
122
127
  /**
123
128
  * In-process cache of FTS indexes observed against the current singleton
124
129
  * connection. Avoids repeated `CALL CREATE_FTS_INDEX` calls, which can trip
@@ -494,6 +499,7 @@ const resetOpenConnectionState = () => {
494
499
  currentDbPath = null;
495
500
  ftsLoaded = false;
496
501
  vectorExtensionLoaded = false;
502
+ vectorIndexEnsured = false;
497
503
  ensuredFTSIndexes.clear();
498
504
  };
499
505
  const runSchemaCreationQueries = async (dbPath) => {
@@ -572,6 +578,7 @@ export const withLbugDb = async (dbPath, operation, options = {}) => {
572
578
  currentDbPath = null;
573
579
  ftsLoaded = false;
574
580
  vectorExtensionLoaded = false;
581
+ vectorIndexEnsured = false;
575
582
  ensuredFTSIndexes.clear();
576
583
  });
577
584
  // Sleep outside the lock — no need to block others while waiting
@@ -596,6 +603,7 @@ const doInitLbug = async (dbPath, readOnly = false) => {
596
603
  currentDbPath = null;
597
604
  ftsLoaded = false;
598
605
  vectorExtensionLoaded = false;
606
+ vectorIndexEnsured = false;
599
607
  ensuredFTSIndexes.clear();
600
608
  }
601
609
  // ---------------------------------------------------------------------------
@@ -1476,6 +1484,7 @@ export const closeLbug = async () => {
1476
1484
  currentDbPath = null;
1477
1485
  ftsLoaded = false;
1478
1486
  vectorExtensionLoaded = false;
1487
+ vectorIndexEnsured = false;
1479
1488
  ensuredFTSIndexes.clear();
1480
1489
  };
1481
1490
  export const isLbugReady = () => conn !== null && db !== null;
@@ -1708,6 +1717,52 @@ export const createFTSIndex = async (tableName, indexName, properties, stemmer =
1708
1717
  throw e;
1709
1718
  }
1710
1719
  };
1720
+ /**
1721
+ * Create the HNSW vector index on the CodeEmbedding table.
1722
+ *
1723
+ * MUST run via `conn.query()` (here through `queryAndDrain`), NOT through the
1724
+ * prepared `executeQuery`/`conn.prepare()` path: `CALL CREATE_VECTOR_INDEX(...)`
1725
+ * compiles to multiple statements, which LadybugDB cannot prepare — it fails
1726
+ * with "Connection Exception: We do not support prepare multiple statements."
1727
+ * Routing index creation through `executeQuery` (prepared) is exactly what
1728
+ * broke vector-index creation during `analyze` (#2114; the singleton
1729
+ * `executeQuery` was switched to the prepared path in #1655 while FTS index
1730
+ * creation kept using `conn.query()`, which is why FTS survived and VECTOR did
1731
+ * not). Mirrors `createFTSIndex` above.
1732
+ *
1733
+ * Returns `true` on success (or when the index already exists — idempotent so
1734
+ * incremental re-runs don't spuriously downgrade to exact scan), `false` when
1735
+ * the VECTOR extension is unavailable or the connection is read-only. Any other
1736
+ * failure propagates so the caller can log it.
1737
+ */
1738
+ export const createVectorIndex = async () => {
1739
+ if (!conn) {
1740
+ throw new Error('LadybugDB not initialized. Call initLbug first.');
1741
+ }
1742
+ // Already built on this connection — skip the round-trip (mirrors createFTSIndex).
1743
+ if (vectorIndexEnsured)
1744
+ return true;
1745
+ if (!(await loadVectorExtension())) {
1746
+ return false;
1747
+ }
1748
+ try {
1749
+ await queryAndDrain(conn, CREATE_VECTOR_INDEX_QUERY);
1750
+ vectorIndexEnsured = true;
1751
+ return true;
1752
+ }
1753
+ catch (e) {
1754
+ const msg = e instanceof Error ? e.message : String(e);
1755
+ // Idempotent: a prior analyze already built the HNSW index.
1756
+ if (msg.includes('already exists')) {
1757
+ vectorIndexEnsured = true;
1758
+ return true;
1759
+ }
1760
+ // Read-only DB (e.g. the MCP query pool): writable analyze owns creation.
1761
+ if (isReadOnlyDbError(e))
1762
+ return false;
1763
+ throw e;
1764
+ }
1765
+ };
1711
1766
  /**
1712
1767
  * Lazy-create an FTS index, caching the fact in-process.
1713
1768
  *
@@ -91,10 +91,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
91
91
  return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
92
92
  }
93
93
 
94
+ /**
95
+ * Whether opt-in diagnostics should be written to the hook's stderr. Strict
96
+ * hook runners validate hook output, so normal, non-error skip paths must stay
97
+ * silent unless the operator explicitly asks for diagnostics via GITNEXUS_DEBUG.
98
+ * See issue #1913.
99
+ */
100
+ function isDebugEnabled() {
101
+ return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
102
+ }
103
+
94
104
  function extractAugmentContext(stderr) {
95
105
  const output = (stderr || '').trim();
96
106
  const marker = output.indexOf('[GitNexus]');
97
- const debug = process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
107
+ const debug = isDebugEnabled();
98
108
  if (debug && output.length > 0) {
99
109
  // Emit the FULL discarded prefix (everything before the marker, or all of
100
110
  // it when no marker is present) so suppressed diagnostics — LadybugDB lock
@@ -258,8 +268,14 @@ function buildAfterToolContext(input) {
258
268
  if (/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) {
259
269
  const hint = buildStaleIndexHint(gitNexusDir, cwd);
260
270
  if (hint) {
261
- process.stderr.write(`${hint}\n`);
271
+ // The hint always reaches the agent via additionalContext (parts). Mirror
272
+ // it to stderr (for terminal users) only under GITNEXUS_DEBUG, so strict
273
+ // hook runners see no unexpected output on this normal path (#1913). The
274
+ // claude hook never mirrored this to stderr — this aligns the two adapters.
262
275
  parts.push(hint);
276
+ if (isDebugEnabled()) {
277
+ process.stderr.write(`${hint}\n`);
278
+ }
263
279
  }
264
280
  }
265
281
  }
@@ -269,7 +285,11 @@ function buildAfterToolContext(input) {
269
285
 
270
286
  function runAugment(gitNexusDir, cwd, pattern) {
271
287
  if (hasGitNexusServerOwner(gitNexusDir)) {
272
- process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
288
+ // Normal skip path: the MCP server owns the DB. Stay silent for strict
289
+ // hook runners (issue #1913); surface the reason only under GITNEXUS_DEBUG.
290
+ if (isDebugEnabled()) {
291
+ process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
292
+ }
273
293
  return '';
274
294
  }
275
295
  const release = acquireHookSlot(gitNexusDir);
@@ -338,7 +358,7 @@ function main() {
338
358
  const handler = handlers[input.hook_event_name || ''];
339
359
  if (handler) handler(input);
340
360
  } catch (err) {
341
- if (process.env.GITNEXUS_DEBUG) {
361
+ if (isDebugEnabled()) {
342
362
  console.error('GitNexus antigravity hook error:', (err.message || '').slice(0, 200));
343
363
  }
344
364
  }
@@ -110,10 +110,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
110
110
  return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
111
111
  }
112
112
 
113
+ /**
114
+ * Whether opt-in diagnostics should be written to the hook's stderr. Strict
115
+ * hook runners (e.g. Codex `PreToolUse`) validate hook output, so normal,
116
+ * non-error skip paths must stay silent unless the operator explicitly asks
117
+ * for diagnostics via GITNEXUS_DEBUG. See issue #1913.
118
+ */
119
+ function isDebugEnabled() {
120
+ return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
121
+ }
122
+
113
123
  function extractAugmentContext(stderr) {
114
124
  const output = (stderr || '').trim();
115
125
  const marker = output.indexOf('[GitNexus]');
116
- const debug = process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
126
+ const debug = isDebugEnabled();
117
127
  if (debug && output.length > 0) {
118
128
  // Emit the FULL discarded prefix (everything before the marker, or all of
119
129
  // it when no marker is present) so suppressed diagnostics — KuzuDB lock
@@ -250,7 +260,12 @@ function handlePreToolUse(input) {
250
260
  const pattern = extractPattern(toolName, toolInput);
251
261
  if (!pattern || pattern.length < 3) return;
252
262
  if (hasGitNexusServerOwner(gitNexusDir)) {
253
- process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
263
+ // Normal skip path: the MCP server owns the DB, so the CLI augment would
264
+ // contend on the lock. Stay silent for strict hook runners (issue #1913);
265
+ // surface the reason only when diagnostics are explicitly requested.
266
+ if (isDebugEnabled()) {
267
+ process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
268
+ }
254
269
  return;
255
270
  }
256
271
 
@@ -361,7 +376,7 @@ function main() {
361
376
  const handler = handlers[input.hook_event_name || ''];
362
377
  if (handler) handler(input);
363
378
  } catch (err) {
364
- if (process.env.GITNEXUS_DEBUG) {
379
+ if (isDebugEnabled()) {
365
380
  console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
366
381
  }
367
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.8-rc.2",
3
+ "version": "1.6.8-rc.4",
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",