nano-brain 2026.8.7 → 2026.8.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-brain",
3
- "version": "2026.8.7",
3
+ "version": "2026.8.9",
4
4
  "description": "Persistent memory and code intelligence for AI coding agents. Local MCP server with self-learning hybrid search (BM25 + vector + knowledge graph + LLM reranking), automatic session ingestion, codebase indexing, and 22 tools. Learns your preferences over time. Works with OpenCode, Claude, Cursor, Windsurf, and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,6 @@
40
40
  "chokidar": "^5.0.0",
41
41
  "fast-glob": "^3.3.3",
42
42
  "p-limit": "^7.3.0",
43
- "sqlite-vec": "^0.1.7-alpha.2",
44
43
  "tree-sitter": "^0.22.4",
45
44
  "tree-sitter-javascript": "^0.23.1",
46
45
  "tree-sitter-python": "^0.23.6",
@@ -7,6 +7,7 @@ import { createStore } from '../store.js';
7
7
  import { hybridSearch } from '../search.js';
8
8
  import { createEmbeddingProvider } from '../embeddings.js';
9
9
  import { generateCorpus, computeCorpusHash } from './generator.js';
10
+ import { QdrantVecStore } from '../providers/qdrant.js';
10
11
  import type {
11
12
  BenchResult,
12
13
  BenchEnvironment,
@@ -134,7 +135,9 @@ function aggregateQuality(
134
135
  async function measureQuality(
135
136
  dbPath: string,
136
137
  groundTruth: GroundTruthQuery[],
137
- ollamaUrl: string | null
138
+ ollamaUrl: string | null,
139
+ qdrantVecStore: QdrantVecStore | null,
140
+ hashToPath: Map<string, string>
138
141
  ): Promise<{ quality: ScaleQuality; latency: Omit<ScaleLatency, 'insert'> }> {
139
142
  const store = createStore(dbPath);
140
143
 
@@ -165,12 +168,15 @@ async function measureQuality(
165
168
  const ftsIds = ftsResults.map(r => docIdFromPath(r.path));
166
169
  ftsPerQuery.push({ query: gt.query, ...computeQueryMetrics(ftsIds, gt.relevant_doc_ids) });
167
170
 
168
- if (embedder) {
171
+ if (embedder && qdrantVecStore) {
169
172
  const t0vec = Date.now();
170
173
  const { embedding } = await embedder.embed(gt.query);
171
- const vecResults = await store.searchVecAsync(gt.query, embedding, { limit: 10 });
174
+ const qdrantResults = await qdrantVecStore.search(embedding, { limit: 10 });
172
175
  vecQueryTimes.push(Date.now() - t0vec);
173
- const vecIds = vecResults.map(r => docIdFromPath(r.path));
176
+ const vecIds = qdrantResults
177
+ .map(r => hashToPath.get(r.hash))
178
+ .filter((p): p is string => p !== undefined)
179
+ .map(p => docIdFromPath(p));
174
180
  vecPerQuery.push({ query: gt.query, ...computeQueryMetrics(vecIds, gt.relevant_doc_ids) });
175
181
 
176
182
  const t0hyb = Date.now();
@@ -213,10 +219,10 @@ async function measureQuality(
213
219
  async function insertDocs(
214
220
  dbPath: string,
215
221
  fixturesDir: string,
216
- ollamaUrl: string | null
217
- ): Promise<LatencyStats> {
222
+ ollamaUrl: string | null,
223
+ qdrantVecStore: QdrantVecStore | null
224
+ ): Promise<{ latency: LatencyStats; hashToPath: Map<string, string> }> {
218
225
  const store = createStore(dbPath);
219
- store.ensureVecTable(768);
220
226
 
221
227
  let embedder: { embed(text: string): Promise<{ embedding: number[] }>; dispose(): void } | null = null;
222
228
  if (ollamaUrl) {
@@ -230,6 +236,7 @@ async function insertDocs(
230
236
  const docsDir = path.join(fixturesDir, 'docs');
231
237
  const docFiles = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
232
238
  const insertTimes: number[] = [];
239
+ const hashToPath = new Map<string, string>();
233
240
  const workspaceRoot = process.cwd();
234
241
  const projectHash = crypto.createHash('sha256').update(workspaceRoot).digest('hex').substring(0, 12);
235
242
 
@@ -252,9 +259,14 @@ async function insertDocs(
252
259
  active: true,
253
260
  projectHash,
254
261
  });
255
- if (embedder) {
262
+ hashToPath.set(hash, docPath);
263
+ if (embedder && qdrantVecStore) {
256
264
  const { embedding } = await embedder.embed(content);
257
- store.insertEmbedding(hash, 0, 0, embedding, 'nomic-embed-text');
265
+ await qdrantVecStore.upsert({
266
+ id: `${hash}:0`,
267
+ embedding,
268
+ metadata: { hash, seq: 0, pos: 0, model: 'nomic-embed-text' },
269
+ });
258
270
  }
259
271
  insertTimes.push(Date.now() - t0);
260
272
  }
@@ -263,7 +275,7 @@ async function insertDocs(
263
275
  store.close();
264
276
  }
265
277
 
266
- return computeLatencyStats(insertTimes);
278
+ return { latency: computeLatencyStats(insertTimes), hashToPath };
267
279
  }
268
280
 
269
281
  async function runCombinationTests(
@@ -380,6 +392,21 @@ async function detectOllamaUrl(): Promise<string | null> {
380
392
  return null;
381
393
  }
382
394
 
395
+ async function detectQdrantUrl(): Promise<string | null> {
396
+ const candidates = [
397
+ process.env['QDRANT_URL'],
398
+ 'http://localhost:6333',
399
+ 'http://host.docker.internal:6333',
400
+ ].filter(Boolean) as string[];
401
+ for (const url of candidates) {
402
+ try {
403
+ const resp = await fetch(`${url}/healthz`, { signal: AbortSignal.timeout(3000) });
404
+ if (resp.ok) return url;
405
+ } catch {}
406
+ }
407
+ return null;
408
+ }
409
+
383
410
  export interface RunOptions {
384
411
  scales: number[];
385
412
  noCleanup: boolean;
@@ -396,6 +423,20 @@ export async function runBenchmarkSuite(opts: RunOptions): Promise<BenchResult>
396
423
  console.warn('Warning: Ollama not reachable — skipping vector and hybrid quality tests');
397
424
  }
398
425
 
426
+ const qdrantUrl = await detectQdrantUrl();
427
+ if (!qdrantUrl) {
428
+ console.warn('[bench] Qdrant unreachable — skipping vector and hybrid test suites');
429
+ }
430
+
431
+ const benchCollectionBaseName = `bench-${Date.now()}`;
432
+ const qdrantVecStore: QdrantVecStore | null = qdrantUrl && ollamaUrl
433
+ ? new QdrantVecStore({ url: qdrantUrl, collection: benchCollectionBaseName, dimensions: 768 })
434
+ : null;
435
+
436
+ if (qdrantVecStore) {
437
+ await qdrantVecStore.ensureCollection();
438
+ }
439
+
399
440
  const ollamaInfo = await getOllamaInfo(ollamaUrl);
400
441
  const env: BenchEnvironment = {
401
442
  ollama_model: ollamaInfo?.model ?? 'none',
@@ -444,10 +485,10 @@ export async function runBenchmarkSuite(opts: RunOptions): Promise<BenchResult>
444
485
  }
445
486
 
446
487
  console.log(' Inserting docs...');
447
- const insertLatency = await insertDocs(testDbPath, fixturesDir, ollamaUrl);
488
+ const { latency: insertLatency, hashToPath } = await insertDocs(testDbPath, fixturesDir, ollamaUrl, qdrantVecStore);
448
489
 
449
490
  console.log(' Running quality metrics...');
450
- const { quality, latency: queryLatency } = await measureQuality(testDbPath, groundTruth, ollamaUrl);
491
+ const { quality, latency: queryLatency } = await measureQuality(testDbPath, groundTruth, ollamaUrl, qdrantVecStore, hashToPath);
451
492
 
452
493
  const scaleLatency: ScaleLatency = {
453
494
  insert: insertLatency,
@@ -479,6 +520,9 @@ export async function runBenchmarkSuite(opts: RunOptions): Promise<BenchResult>
479
520
  };
480
521
  }
481
522
  } finally {
523
+ if (qdrantVecStore) {
524
+ try { await qdrantVecStore.deleteCollection(); } catch {}
525
+ }
482
526
  if (!noCleanup) {
483
527
  try { fs.unlinkSync(testDbPath); } catch {}
484
528
  try { fs.unlinkSync(testDbPath + '-wal'); } catch {}
package/src/bench.ts CHANGED
@@ -111,7 +111,7 @@ async function runSearchSuite(
111
111
  const result = await embedder.embed('error handling async');
112
112
  cachedEmbedding = result.embedding;
113
113
  }
114
- store.searchVec('error handling async', cachedEmbedding, { limit: 10 });
114
+ await store.searchVecAsync('error handling async', cachedEmbedding, { limit: 10 });
115
115
  }, iterations)
116
116
  );
117
117
 
@@ -71,7 +71,6 @@ export async function handleEmbed(globalOpts: GlobalOptions, commandArgs: string
71
71
  if (qdrantStore) store.setVectorStore(qdrantStore);
72
72
  const hashes = store.getHashesNeedingEmbedding();
73
73
  if (hashes.length > 0) {
74
- store.ensureVecTable(provider.getDimensions());
75
74
  cliOutput(`[${path.basename(process.cwd())}] ${hashes.length} chunks pending...`);
76
75
  const embedded = await embedPendingCodebase(store, provider, 50);
77
76
  totalEmbedded += embedded;
@@ -97,7 +96,6 @@ export async function handleEmbed(globalOpts: GlobalOptions, commandArgs: string
97
96
  if (qdrantStore) wsStore.setVectorStore(qdrantStore);
98
97
  const wsHashes = wsStore.getHashesNeedingEmbedding();
99
98
  if (wsHashes.length > 0) {
100
- wsStore.ensureVecTable(provider.getDimensions());
101
99
  cliOutput(`[${path.basename(wsPath)}] ${wsHashes.length} chunks pending...`);
102
100
  const embedded = await embedPendingCodebase(wsStore, provider, 50);
103
101
  totalEmbedded += embedded;
@@ -221,7 +221,6 @@ export async function handleInit(globalOpts: GlobalOptions, commandArgs: string[
221
221
  const provider = await createEmbeddingProvider({ embeddingConfig });
222
222
  const INIT_EMBED_CAP = 50;
223
223
  if (provider) {
224
- store.ensureVecTable(provider.getDimensions());
225
224
  let embedded = 0;
226
225
  while (embedded < INIT_EMBED_CAP) {
227
226
  const row = store.getNextHashNeedingEmbedding(projectHash);