grepmax 0.26.37 → 0.26.40

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.37",
13
+ "version": "0.26.40",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.ProjectBatchProcessor = void 0;
46
+ exports.applyReusedEmbeddings = applyReusedEmbeddings;
46
47
  const path = __importStar(require("node:path"));
47
48
  const config_1 = require("../../config");
48
49
  const vector_db_1 = require("../store/vector-db");
@@ -172,6 +173,18 @@ class ProjectBatchProcessor {
172
173
  recentReindexed,
173
174
  };
174
175
  }
176
+ loadReusableEmbeddings(absPath) {
177
+ return __awaiter(this, void 0, void 0, function* () {
178
+ // An optimisation only: a failed lookup costs a full re-embed, nothing more.
179
+ try {
180
+ return yield this.vectorDb.getReusableEmbeddings(absPath);
181
+ }
182
+ catch (err) {
183
+ (0, logger_1.debug)(this.wtag, `embedding reuse lookup failed for ${absPath}: ${err instanceof Error ? err.message : String(err)}`);
184
+ return undefined;
185
+ }
186
+ });
187
+ }
175
188
  close() {
176
189
  return __awaiter(this, void 0, void 0, function* () {
177
190
  var _a;
@@ -360,11 +373,14 @@ class ProjectBatchProcessor {
360
373
  return;
361
374
  }
362
375
  }
363
- const result = yield pool.processFile({
364
- path: absPath,
365
- absolutePath: absPath,
366
- projectRoot: this.projectRoot,
367
- }, batchAc.signal);
376
+ // A forced reprocess is a repair; it must not trust what is stored.
377
+ const reusable = !forceReprocess && (cached === null || cached === void 0 ? void 0 : cached.hasVectors) !== false && (cached === null || cached === void 0 ? void 0 : cached.hash)
378
+ ? yield this.loadReusableEmbeddings(absPath)
379
+ : undefined;
380
+ const result = yield pool.processFile(Object.assign({ path: absPath, absolutePath: absPath, projectRoot: this.projectRoot }, ((reusable === null || reusable === void 0 ? void 0 : reusable.size)
381
+ ? { reusableKeys: Array.from(reusable.keys()) }
382
+ : {})), batchAc.signal);
383
+ applyReusedEmbeddings(result, reusable);
368
384
  // Policy and filesystem state can change while embedding is in
369
385
  // flight. Never commit a result that no longer describes the path.
370
386
  const current = yield this.filePolicy.classifyFile(absPath);
@@ -652,3 +668,25 @@ class ProjectBatchProcessor {
652
668
  }
653
669
  exports.ProjectBatchProcessor = ProjectBatchProcessor;
654
670
  ProjectBatchProcessor.RECENT_BATCH_WINDOW = 5;
671
+ /**
672
+ * Fill the records a worker left unembedded from the embeddings offered to it.
673
+ * Throws when one cannot be filled, so the file is retried rather than stored
674
+ * with an empty vector.
675
+ */
676
+ function applyReusedEmbeddings(result, reusable) {
677
+ var _a;
678
+ if (!((_a = result.reused) === null || _a === void 0 ? void 0 : _a.length))
679
+ return;
680
+ for (const { index, key } of result.reused) {
681
+ const record = result.vectors[index];
682
+ const embedding = reusable === null || reusable === void 0 ? void 0 : reusable.get(key);
683
+ if (!record || !embedding) {
684
+ throw new Error(`worker reused an embedding that was not offered (chunk ${index})`);
685
+ }
686
+ record.vector = embedding.vector;
687
+ record.colbert = embedding.colbert;
688
+ record.colbert_scale = embedding.colbert_scale;
689
+ record.pooled_colbert_48d = embedding.pooled_colbert_48d;
690
+ record.doc_token_ids = embedding.doc_token_ids;
691
+ }
692
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.embeddingReuseKey = embeddingReuseKey;
4
+ exports.toFloat32 = toFloat32;
5
+ exports.toInt32 = toInt32;
6
+ exports.toBytes = toBytes;
7
+ const node_crypto_1 = require("node:crypto");
8
+ /** Keyed by the exact text passed to the embedder (`PreparedChunk.content`). */
9
+ function embeddingReuseKey(content) {
10
+ return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
11
+ }
12
+ function unwrap(val) {
13
+ if (val &&
14
+ !ArrayBuffer.isView(val) &&
15
+ typeof val.toArray === "function") {
16
+ return val.toArray();
17
+ }
18
+ return val;
19
+ }
20
+ // Copies rather than views: an Arrow view pins its whole record batch buffer.
21
+ function toFloat32(val) {
22
+ const v = unwrap(val);
23
+ if (v instanceof Float32Array)
24
+ return v.slice();
25
+ if (ArrayBuffer.isView(v) || Array.isArray(v)) {
26
+ return Float32Array.from(v);
27
+ }
28
+ return new Float32Array(0);
29
+ }
30
+ function toInt32(val) {
31
+ const v = unwrap(val);
32
+ if (v instanceof Int32Array)
33
+ return v.slice();
34
+ if (ArrayBuffer.isView(v) || Array.isArray(v)) {
35
+ return Int32Array.from(v);
36
+ }
37
+ return new Int32Array(0);
38
+ }
39
+ function toBytes(val) {
40
+ const v = unwrap(val);
41
+ if (ArrayBuffer.isView(v)) {
42
+ return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength).slice());
43
+ }
44
+ if (Array.isArray(v))
45
+ return Buffer.from(v);
46
+ return Buffer.alloc(0);
47
+ }
@@ -51,6 +51,7 @@ const fs = __importStar(require("node:fs"));
51
51
  const lancedb = __importStar(require("@lancedb/lancedb"));
52
52
  const apache_arrow_1 = require("apache-arrow");
53
53
  const config_1 = require("../../config");
54
+ const embedding_reuse_1 = require("../index/embedding-reuse");
54
55
  const index_config_1 = require("../index/index-config");
55
56
  const cleanup_1 = require("../utils/cleanup");
56
57
  const filter_builder_1 = require("../utils/filter-builder");
@@ -304,10 +305,15 @@ class VectorDB {
304
305
  (0, logger_1.log)("vectordb", `Periodic maintenance failed: ${err}`);
305
306
  }
306
307
  }));
307
- this.maintenancePromise = run.finally(() => {
308
- if (this.maintenancePromise === run)
308
+ // Compare against the promise actually stored, not `run`: `.finally()`
309
+ // returns a new promise, so checking `run` never matched and the field
310
+ // latched non-null forever — isMaintenanceActive() then read true for the
311
+ // daemon's whole life, silently blocking every recycle and idle shutdown.
312
+ const tracked = run.finally(() => {
313
+ if (this.maintenancePromise === tracked)
309
314
  this.maintenancePromise = null;
310
315
  });
316
+ this.maintenancePromise = tracked;
311
317
  }, MAINTENANCE_INTERVAL_MS);
312
318
  this.maintenanceTimer.unref();
313
319
  }
@@ -1430,6 +1436,48 @@ class VectorDB {
1430
1436
  return unique;
1431
1437
  });
1432
1438
  }
1439
+ /**
1440
+ * The stored embeddings of one file, keyed by `embeddingReuseKey(content)`.
1441
+ * Rows whose dense width disagrees with this table are left out rather than
1442
+ * offered — reuse must never be the way a wrong-width vector gets copied.
1443
+ */
1444
+ getReusableEmbeddings(filePath) {
1445
+ return __awaiter(this, void 0, void 0, function* () {
1446
+ var _a;
1447
+ const table = yield this.ensureTable();
1448
+ const rows = yield table
1449
+ .query()
1450
+ .select([
1451
+ "content",
1452
+ "vector",
1453
+ "colbert",
1454
+ "colbert_scale",
1455
+ "pooled_colbert_48d",
1456
+ "doc_token_ids",
1457
+ ])
1458
+ .where(`path = '${(0, filter_builder_1.escapeSqlString)(filePath)}'`)
1459
+ .toArray();
1460
+ const reusable = new Map();
1461
+ for (const row of rows) {
1462
+ const vector = (0, embedding_reuse_1.toFloat32)(row.vector);
1463
+ const colbert = (0, embedding_reuse_1.toBytes)(row.colbert);
1464
+ if (vector.length !== this.vectorDim || colbert.length === 0)
1465
+ continue;
1466
+ const pooled = row.pooled_colbert_48d == null
1467
+ ? undefined
1468
+ : (0, embedding_reuse_1.toFloat32)(row.pooled_colbert_48d);
1469
+ const tokenIds = row.doc_token_ids == null ? undefined : (0, embedding_reuse_1.toInt32)(row.doc_token_ids);
1470
+ reusable.set((0, embedding_reuse_1.embeddingReuseKey)(String((_a = row.content) !== null && _a !== void 0 ? _a : "")), {
1471
+ vector,
1472
+ colbert,
1473
+ colbert_scale: typeof row.colbert_scale === "number" ? row.colbert_scale : 1,
1474
+ pooled_colbert_48d: (pooled === null || pooled === void 0 ? void 0 : pooled.length) ? pooled : undefined,
1475
+ doc_token_ids: tokenIds,
1476
+ });
1477
+ }
1478
+ return reusable;
1479
+ });
1480
+ }
1433
1481
  getStats() {
1434
1482
  return __awaiter(this, void 0, void 0, function* () {
1435
1483
  const table = yield this.ensureTable();
@@ -52,6 +52,7 @@ const uuid_1 = require("uuid");
52
52
  const config_1 = require("../../config");
53
53
  const chunker_1 = require("../index/chunker");
54
54
  const embedding_generation_1 = require("../index/embedding-generation");
55
+ const embedding_reuse_1 = require("../index/embedding-reuse");
55
56
  const skeleton_1 = require("../skeleton");
56
57
  const file_utils_1 = require("../utils/file-utils");
57
58
  const logger_1 = require("../utils/logger");
@@ -234,6 +235,7 @@ class WorkerOrchestrator {
234
235
  }
235
236
  processFile(input, onProgress) {
236
237
  return __awaiter(this, void 0, void 0, function* () {
238
+ var _a;
237
239
  const fileStart = performance.now();
238
240
  const absolutePath = path.isAbsolute(input.path)
239
241
  ? input.path
@@ -281,12 +283,32 @@ class WorkerOrchestrator {
281
283
  return { vectors: [], hash, mtimeMs, size };
282
284
  }
283
285
  const preparedChunks = this.toPreparedChunks(input.path, hash, chunks, skeletonResult.success ? skeletonResult.skeleton : undefined);
286
+ const reusableKeys = ((_a = input.reusableKeys) === null || _a === void 0 ? void 0 : _a.length)
287
+ ? new Set(input.reusableKeys)
288
+ : null;
289
+ const reused = [];
290
+ const toEmbed = [];
291
+ preparedChunks.forEach((chunk, idx) => {
292
+ if (reusableKeys) {
293
+ const key = (0, embedding_reuse_1.embeddingReuseKey)(chunk.content);
294
+ if (reusableKeys.has(key)) {
295
+ reused.push({ index: idx, key });
296
+ return;
297
+ }
298
+ }
299
+ toEmbed.push(idx);
300
+ });
284
301
  const embedStart = performance.now();
285
- const hybrids = yield this.computeHybrid(preparedChunks.map((chunk) => chunk.content), onProgress);
286
- (0, logger_1.debug)("orch", `embedded ${input.path} → ${hybrids.length} vectors ${(performance.now() - embedStart).toFixed(0)}ms`);
302
+ const embedded = yield this.computeHybrid(toEmbed.map((idx) => preparedChunks[idx].content), onProgress);
303
+ const hybrids = new Map();
304
+ toEmbed.forEach((chunkIdx, i) => {
305
+ if (embedded[i])
306
+ hybrids.set(chunkIdx, embedded[i]);
307
+ });
308
+ (0, logger_1.debug)("orch", `embedded ${input.path} → ${embedded.length} vectors (${reused.length} reused) ${(performance.now() - embedStart).toFixed(0)}ms`);
287
309
  const vectors = preparedChunks.map((chunk, idx) => {
288
310
  var _a;
289
- const hybrid = (_a = hybrids[idx]) !== null && _a !== void 0 ? _a : {
311
+ const hybrid = (_a = hybrids.get(idx)) !== null && _a !== void 0 ? _a : {
290
312
  dense: new Float32Array(),
291
313
  colbert: new Int8Array(),
292
314
  scale: 1,
@@ -295,7 +317,9 @@ class WorkerOrchestrator {
295
317
  });
296
318
  onProgress === null || onProgress === void 0 ? void 0 : onProgress();
297
319
  (0, logger_1.debug)("orch", `processFile done: ${input.path} ${vectors.length} vectors ${(performance.now() - fileStart).toFixed(0)}ms`);
298
- return { vectors, hash, mtimeMs, size };
320
+ return reused.length
321
+ ? { vectors, hash, mtimeMs, size, reused }
322
+ : { vectors, hash, mtimeMs, size };
299
323
  });
300
324
  }
301
325
  encodeQuery(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.37",
3
+ "version": "0.26.40",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -40,7 +40,7 @@
40
40
  "typecheck": "tsc --noEmit",
41
41
  "test:typecheck": "tsc -p tsconfig.tests.json",
42
42
  "prepublishOnly": "pnpm run test:typecheck && pnpm build",
43
- "preversion": "pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check",
43
+ "preversion": "pnpm audit --prod && pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check",
44
44
  "version": "bash scripts/sync-versions.sh && git add -A",
45
45
  "postversion": "bash scripts/postrelease.sh"
46
46
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.37",
3
+ "version": "0.26.40",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",