@comfanion/usethis_search 3.0.0-dev.24 → 3.0.0-dev.25

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": "@comfanion/usethis_search",
3
- "version": "3.0.0-dev.24",
3
+ "version": "3.0.0-dev.25",
4
4
  "description": "OpenCode plugin: semantic search with graph-based context (v3: graph relations, 1-hop context, LSP + regex analyzers)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/tools/search.ts CHANGED
@@ -10,7 +10,7 @@ import { tool } from "@opencode-ai/plugin"
10
10
  import path from "path"
11
11
  import fs from "fs/promises"
12
12
 
13
- import { CodebaseIndexer, getSearchConfig } from "../vectorizer/index.ts"
13
+ import { CodebaseIndexer, getSearchConfig, getIndexer, releaseIndexer } from "../vectorizer/index.ts"
14
14
 
15
15
  // ── Extension → language mapping (for filter parsing) ─────────────────────
16
16
  const EXT_TO_LANG: Record<string, string> = {
@@ -133,9 +133,9 @@ Examples:
133
133
  // Freshen from config (default: false — auto_index handles it)
134
134
  if (cfg.freshen) {
135
135
  try {
136
- const tempIndexer = await new CodebaseIndexer(projectRoot, indexName).init()
137
- await tempIndexer.freshen()
138
- await tempIndexer.unloadModel()
136
+ const indexer = await getIndexer(projectRoot, indexName)
137
+ await indexer.freshen()
138
+ releaseIndexer(projectRoot, indexName)
139
139
  } catch {
140
140
  // non-fatal — search can proceed without freshen
141
141
  }
@@ -144,19 +144,22 @@ Examples:
144
144
  let allResults: any[] = []
145
145
 
146
146
  if (args.searchAll) {
147
- const tempIndexer = await new CodebaseIndexer(projectRoot, "code").init()
147
+ const tempIndexer = await getIndexer(projectRoot, "code")
148
148
  const indexes = await tempIndexer.listIndexes()
149
- await tempIndexer.unloadModel()
149
+ releaseIndexer(projectRoot, "code")
150
150
 
151
151
  if (indexes.length === 0) {
152
152
  return `No indexes found. The codebase needs to be indexed first.\n\nRun the CLI: bunx usethis_search reindex`
153
153
  }
154
154
 
155
155
  for (const idx of indexes) {
156
- const indexer = await new CodebaseIndexer(projectRoot, idx).init()
157
- const results = await indexer.search(args.query, limit, includeArchived, searchOptions)
158
- allResults.push(...results.map((r: any) => ({ ...r, _index: idx })))
159
- await indexer.unloadModel()
156
+ const indexer = await getIndexer(projectRoot, idx)
157
+ try {
158
+ const results = await indexer.search(args.query, limit, includeArchived, searchOptions)
159
+ allResults.push(...results.map((r: any) => ({ ...r, _index: idx })))
160
+ } finally {
161
+ releaseIndexer(projectRoot, idx)
162
+ }
160
163
  }
161
164
 
162
165
  allResults.sort((a, b) => {
@@ -171,9 +174,9 @@ Examples:
171
174
  await fs.access(hashesFile)
172
175
  } catch {
173
176
  // Index doesn't exist — check what indexes ARE available
174
- const tempIndexer = await new CodebaseIndexer(projectRoot, "code").init()
177
+ const tempIndexer = await getIndexer(projectRoot, "code")
175
178
  const available = await tempIndexer.listIndexes()
176
- await tempIndexer.unloadModel()
179
+ releaseIndexer(projectRoot, "code")
177
180
 
178
181
  if (available.length > 0) {
179
182
  const list = available.map(i => `"${i}"`).join(", ")
@@ -182,10 +185,13 @@ Examples:
182
185
  return `No indexes found. The codebase needs to be indexed first.\n\nRun the CLI: bunx usethis_search reindex`
183
186
  }
184
187
 
185
- const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
186
- const results = await indexer.search(args.query, limit, includeArchived, searchOptions)
187
- allResults = results.map((r: any) => ({ ...r, _index: indexName }))
188
- await indexer.unloadModel()
188
+ const indexer = await getIndexer(projectRoot, indexName)
189
+ try {
190
+ const results = await indexer.search(args.query, limit, includeArchived, searchOptions)
191
+ allResults = results.map((r: any) => ({ ...r, _index: indexName }))
192
+ } finally {
193
+ releaseIndexer(projectRoot, indexName)
194
+ }
189
195
  }
190
196
 
191
197
  // ── Score cutoff — drop low-relevance results ──────────────────────────
@@ -460,17 +460,24 @@ class CodebaseIndexer {
460
460
  await this.loadHashes();
461
461
 
462
462
  // Graph DB — only if graph is enabled in config
463
+ // Non-fatal: if LevelDB lock fails (parallel access), search works without graph
463
464
  if (GRAPH_CONFIG.enabled) {
464
- const graphType = this.indexName === "docs" ? "doc_graph" : "code_graph";
465
- const graphPath = path.join(this.root, ".opencode", "graph", graphType);
466
- await fs.mkdir(path.dirname(graphPath), { recursive: true });
467
- this.graphDB = await new GraphDB(graphPath).init();
468
- this.graphBuilder = new GraphBuilder(
469
- this.graphDB,
470
- this.root,
471
- GRAPH_CONFIG.lsp.enabled,
472
- GRAPH_CONFIG.lsp.timeout_ms,
473
- );
465
+ try {
466
+ const graphType = this.indexName === "docs" ? "doc_graph" : "code_graph";
467
+ const graphPath = path.join(this.root, ".opencode", "graph", graphType);
468
+ await fs.mkdir(path.dirname(graphPath), { recursive: true });
469
+ this.graphDB = await new GraphDB(graphPath).init();
470
+ this.graphBuilder = new GraphBuilder(
471
+ this.graphDB,
472
+ this.root,
473
+ GRAPH_CONFIG.lsp.enabled,
474
+ GRAPH_CONFIG.lsp.timeout_ms,
475
+ );
476
+ } catch (e) {
477
+ if (DEBUG) console.log(`[vectorizer] GraphDB init failed (lock?): ${e.message || e}`);
478
+ this.graphDB = null;
479
+ this.graphBuilder = null;
480
+ }
474
481
  }
475
482
 
476
483
  // Usage tracker — provenance & usage stats
@@ -1327,4 +1334,65 @@ function getSearchConfig() {
1327
1334
  return SEARCH_CONFIG;
1328
1335
  }
1329
1336
 
1330
- export { CodebaseIndexer, INDEX_PRESETS, getEmbeddingModel, getSearchConfig };
1337
+ // ── Singleton indexer pool ──────────────────────────────────────────────────
1338
+ // Prevents LevelDB lock conflicts when parallel searches hit the same index.
1339
+ // Each unique (projectRoot, indexName) gets one shared CodebaseIndexer.
1340
+ const _indexerPool = new Map<string, { indexer: CodebaseIndexer; refCount: number; initPromise: Promise<CodebaseIndexer> }>();
1341
+
1342
+ /**
1343
+ * Get or create a shared CodebaseIndexer for the given project + index.
1344
+ * Multiple callers get the same instance — no LevelDB lock conflicts.
1345
+ *
1346
+ * Usage:
1347
+ * const indexer = await getIndexer(projectRoot, "code");
1348
+ * try {
1349
+ * const results = await indexer.search(...);
1350
+ * } finally {
1351
+ * releaseIndexer(projectRoot, "code");
1352
+ * }
1353
+ */
1354
+ async function getIndexer(projectRoot: string, indexName: string = "code"): Promise<CodebaseIndexer> {
1355
+ const key = `${projectRoot}::${indexName}`;
1356
+ const existing = _indexerPool.get(key);
1357
+ if (existing) {
1358
+ existing.refCount++;
1359
+ return existing.initPromise;
1360
+ }
1361
+
1362
+ const indexer = new CodebaseIndexer(projectRoot, indexName);
1363
+ const initPromise = indexer.init();
1364
+ _indexerPool.set(key, { indexer, refCount: 1, initPromise });
1365
+ return initPromise;
1366
+ }
1367
+
1368
+ /**
1369
+ * Release a reference to a shared indexer. When refCount reaches 0,
1370
+ * the indexer is kept alive (for future reuse) but model memory is freed.
1371
+ * Call destroyIndexer() to fully close and remove from pool.
1372
+ */
1373
+ function releaseIndexer(projectRoot: string, indexName: string = "code") {
1374
+ const key = `${projectRoot}::${indexName}`;
1375
+ const entry = _indexerPool.get(key);
1376
+ if (!entry) return;
1377
+ entry.refCount = Math.max(0, entry.refCount - 1);
1378
+ // Keep in pool — don't unload. Next search reuses the same instance.
1379
+ }
1380
+
1381
+ /**
1382
+ * Fully close and remove an indexer from the pool.
1383
+ * Used by CLI clear/reindex operations that need a fresh state.
1384
+ */
1385
+ async function destroyIndexer(projectRoot: string, indexName: string = "code") {
1386
+ const key = `${projectRoot}::${indexName}`;
1387
+ const entry = _indexerPool.get(key);
1388
+ if (!entry) return;
1389
+ _indexerPool.delete(key);
1390
+ try {
1391
+ const indexer = await entry.initPromise;
1392
+ await indexer.unloadModel();
1393
+ } catch {
1394
+ // best effort
1395
+ }
1396
+ }
1397
+
1398
+ export { CodebaseIndexer, INDEX_PRESETS, getEmbeddingModel, getSearchConfig, getIndexer, releaseIndexer, destroyIndexer };