grepmax 0.26.11 → 0.26.13

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.11",
13
+ "version": "0.26.13",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -88,7 +88,7 @@ exports.doctor = new commander_1.Command("doctor")
88
88
  .option("--fix", "Auto-fix detected issues (compact, prune, remove stale locks)", false)
89
89
  .option("--agent", "Compact output for AI agents", false)
90
90
  .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
91
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
91
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
92
92
  if (!opts.agent)
93
93
  console.log("gmax Doctor\n");
94
94
  const root = config_1.PATHS.globalRoot;
@@ -198,7 +198,7 @@ exports.doctor = new commander_1.Command("doctor")
198
198
  let staleLock = false;
199
199
  const orphanedProjects = [];
200
200
  try {
201
- const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
201
+ const { VectorDB, findStaleLanceTempFiles, sweepStaleLanceTempFiles } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
202
202
  const db = new VectorDB(config_1.PATHS.lancedbDir);
203
203
  const table = yield db.ensureTable();
204
204
  const totalChunks = yield table.countRows();
@@ -258,7 +258,16 @@ exports.doctor = new commander_1.Command("doctor")
258
258
  }
259
259
  }
260
260
  // Compute warning flags
261
- const bloatRatio = logicalSize > 0 ? diskSize / logicalSize : 0;
261
+ //
262
+ // Abandoned `.tmp*` scratch files are disk that optimize provably cannot
263
+ // reclaim (no manifest entry, so `deleteUnverified` will not touch them).
264
+ // They get their own remediation — a sweep — and are excluded from the
265
+ // ratio that decides whether to run a whole-table optimize, because
266
+ // counting them there prescribes a slow rewrite for bytes it cannot free.
267
+ const staleTempFiles = findStaleLanceTempFiles(config_1.PATHS.lancedbDir);
268
+ const staleTempBytes = staleTempFiles.reduce((n, f) => n + f.size, 0);
269
+ const reclaimableDiskSize = Math.max(0, diskSize - staleTempBytes);
270
+ const bloatRatio = logicalSize > 0 ? reclaimableDiskSize / logicalSize : 0;
262
271
  if (bloatRatio > 2.0)
263
272
  needsOptimize = true;
264
273
  // Track the compactor's own threshold. Warning (and worse, --fix forcing a
@@ -281,7 +290,7 @@ exports.doctor = new commander_1.Command("doctor")
281
290
  ? "LOW"
282
291
  : "ok";
283
292
  }
284
- catch (_o) { }
293
+ catch (_p) { }
285
294
  const staleChunkerProjects = projects.filter((p) => {
286
295
  var _a;
287
296
  return p.status === "indexed" &&
@@ -304,6 +313,8 @@ exports.doctor = new commander_1.Command("doctor")
304
313
  `lock=${lockStatus.split(" ")[0]}`,
305
314
  `daemon=${daemonUp ? "running" : "stopped"}`,
306
315
  `orphaned=${orphanedProjects.length}`,
316
+ `stale_temp=${staleTempFiles.length}`,
317
+ `stale_temp_bytes=${staleTempBytes}`,
307
318
  `stale_chunker=${staleChunkerProjects.length}`,
308
319
  `stale_embedding=${staleEmbeddingProjects.length}`,
309
320
  `legacy_embedding=${legacyEmbeddingCount}`,
@@ -380,11 +391,15 @@ exports.doctor = new commander_1.Command("doctor")
380
391
  console.log(`ok Disk: ${formatSize(availBytes)} available`);
381
392
  }
382
393
  // Storage
394
+ const storageLine = `${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk`;
383
395
  if (bloatRatio > 2.0) {
384
- console.log(`WARN Storage: ${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk (${bloatRatio.toFixed(1)}x — orphaned files)`);
396
+ console.log(`WARN Storage: ${storageLine} (${bloatRatio.toFixed(1)}x — orphaned files)`);
385
397
  }
386
398
  else {
387
- console.log(`ok Storage: ${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk`);
399
+ console.log(`ok Storage: ${storageLine}`);
400
+ }
401
+ if (staleTempFiles.length > 0) {
402
+ console.log(`WARN Abandoned temp files: ${staleTempFiles.length} (${formatSize(staleTempBytes)}) — leftover from interrupted compaction, run with --fix`);
388
403
  }
389
404
  // Fragments
390
405
  if (numSmallFragments > config_1.FRAGMENT_COMPACT_THRESHOLD) {
@@ -486,7 +501,7 @@ exports.doctor = new commander_1.Command("doctor")
486
501
  yield mc.close();
487
502
  }
488
503
  }
489
- catch (_p) { }
504
+ catch (_q) { }
490
505
  }
491
506
  }
492
507
  // --fix auto-remediation
@@ -500,12 +515,52 @@ exports.doctor = new commander_1.Command("doctor")
500
515
  console.log("ok Removed stale lock");
501
516
  fixed++;
502
517
  }
518
+ // Sweep before optimize: these bytes are unreachable by optimize, and
519
+ // clearing them first means any bloat still visible afterwards is real.
520
+ // Unlike optimize this needs no single-writer routing — the age gate in
521
+ // findStaleLanceTempFiles already excludes anything a writer could hold.
522
+ if (staleTempFiles.length > 0) {
523
+ const swept = sweepStaleLanceTempFiles(config_1.PATHS.lancedbDir);
524
+ if (!opts.agent)
525
+ console.log(`ok Removed ${swept.filesRemoved} abandoned temp file(s), freed ${formatSize(swept.bytesFreed)}`);
526
+ fixed++;
527
+ }
503
528
  if (needsOptimize) {
504
529
  if (!opts.agent)
505
530
  console.log("... Running optimize (compact + prune)...");
506
- yield db.optimize(3, 0);
507
- if (!opts.agent)
508
- console.log("ok Optimize complete");
531
+ // Prefer the daemon. Lance's optimize deletes unverified files, which
532
+ // is only safe with a single writer on the store — optimizing here
533
+ // while the daemon compacts made this command a second writer. With a
534
+ // daemon up it owns compaction; without one we are the sole process
535
+ // and can do it in-process safely.
536
+ const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
537
+ const resp = yield sendDaemonCommand({ cmd: "optimize" }, { timeoutMs: 10 * 60 * 1000 });
538
+ let via = "daemon";
539
+ if (!resp.ok) {
540
+ // Only fall back when the daemon could not take it. A daemon that
541
+ // answered with a real failure should surface, not be retried here
542
+ // as a competing writer.
543
+ const reason = String((_k = resp.error) !== null && _k !== void 0 ? _k : "");
544
+ // Fall back ONLY when nothing is listening. A daemon that is merely
545
+ // too old ("unknown command") or busy ("timeout") is still a live
546
+ // writer on the store, so optimizing here would recreate the
547
+ // two-writer case this routing exists to prevent.
548
+ const daemonAbsent = reason.includes("ECONNREFUSED") || reason.includes("ENOENT");
549
+ if (daemonAbsent) {
550
+ yield db.optimize(3, 0);
551
+ via = "in-process (no daemon)";
552
+ }
553
+ else if (reason.includes("unknown command")) {
554
+ via = "";
555
+ console.log("WARN Optimize skipped: the running daemon predates the optimize command. Restart it (gmax watch --daemon -b) and re-run.");
556
+ }
557
+ else {
558
+ via = "";
559
+ console.log(`WARN Optimize failed via daemon: ${reason}`);
560
+ }
561
+ }
562
+ if (via && !opts.agent)
563
+ console.log(`ok Optimize complete (${via})`);
509
564
  fixed++;
510
565
  }
511
566
  if (orphanedProjects.length > 0) {
@@ -533,7 +588,7 @@ exports.doctor = new commander_1.Command("doctor")
533
588
  const done = yield sendStreamingCommand({ cmd: "index", root: p.root, reset: true }, () => { }).catch((e) => ({ ok: false, error: String(e) }));
534
589
  if (done.ok &&
535
590
  (!("degraded" in done) || done.degraded !== true)) {
536
- const chunks = (_k = done.indexed) !== null && _k !== void 0 ? _k : 0;
591
+ const chunks = (_l = done.indexed) !== null && _l !== void 0 ? _l : 0;
537
592
  if (opts.agent) {
538
593
  console.log(`stale_chunker_reindexed\tname=${name}\tchunks=${chunks}`);
539
594
  }
@@ -545,7 +600,7 @@ exports.doctor = new commander_1.Command("doctor")
545
600
  else if (!opts.agent) {
546
601
  const degraded = "degraded" in done && done.degraded === true;
547
602
  const reason = degraded
548
- ? `${(_l = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _l !== void 0 ? _l : 0} scan errors, ${(_m = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _m !== void 0 ? _m : 0} file failures`
603
+ ? `${(_m = ("scanErrors" in done ? done.scanErrors : 0)) !== null && _m !== void 0 ? _m : 0} scan errors, ${(_o = ("failedFiles" in done ? done.failedFiles : 0)) !== null && _o !== void 0 ? _o : 0} file failures`
549
604
  : done.error;
550
605
  console.log(`FAIL ${name}: reindex failed (${reason})`);
551
606
  }
package/dist/config.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.INDEXABLE_EXTENSIONS = exports.VERSION_PRUNE_THRESHOLD = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.REBUILD_COMMAND = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
36
+ exports.INDEXABLE_EXTENSIONS = exports.STALE_TEMP_FILE_AGE_MS = exports.VERSION_PRUNE_THRESHOLD = exports.FRAGMENT_COMPACT_THRESHOLD = exports.DISK_LOW_BYTES = exports.DISK_CRITICAL_BYTES = exports.MAX_FILE_SIZE_BYTES = exports.PATHS = exports.MAX_WORKER_MEMORY_MB = exports.WORKER_BOOT_TIMEOUT_MS = exports.WORKER_TIMEOUT_MS = exports.REBUILD_COMMAND = exports.CHUNKER_VERSION_HISTORY = exports.CONFIG = exports.MODEL_IDS = exports.DEFAULT_MODEL_TIER = exports.MODEL_TIERS = void 0;
37
37
  exports.describeChunkerGap = describeChunkerGap;
38
38
  exports.describeEmbeddingGap = describeEmbeddingGap;
39
39
  exports.describeSchemaDimGap = describeSchemaDimGap;
@@ -281,6 +281,19 @@ exports.FRAGMENT_COMPACT_THRESHOLD = 400;
281
281
  // fire on every healthy store — and make `doctor --fix` force the whole-table
282
282
  // rewrite that the fragment threshold exists to avoid.
283
283
  exports.VERSION_PRUNE_THRESHOLD = 2000;
284
+ // Age past which a Lance `.tmp*` scratch file in a `*.lance/data` directory is
285
+ // considered abandoned rather than in flight. Lance writes a new fragment to a
286
+ // dot-prefixed temp file and renames it on success, so a killed optimize leaves
287
+ // the temp behind forever: it is in no manifest, so `optimize({deleteUnverified})`
288
+ // never collects it, and the dot prefix hides it from a plain `ls`. Three such
289
+ // files (7.3 GB, up to 2.5 months old) were what kept this store pinned at a 2.1x
290
+ // disk/logical ratio that repeated optimize passes could not move.
291
+ //
292
+ // The age gate is the whole safety argument: an in-flight write keeps bumping its
293
+ // temp file's mtime, so anything this stale has no writer, and deleting it is safe
294
+ // without knowing how many processes are on the store. Observed optimize runs on a
295
+ // 325k-row store peak at ~1.4 min, so an hour is ~40x the worst case seen.
296
+ exports.STALE_TEMP_FILE_AGE_MS = 60 * 60 * 1000;
284
297
  // Extensions we consider for indexing to avoid binary noise and improve relevance.
285
298
  exports.INDEXABLE_EXTENSIONS = new Set([
286
299
  ".ts",
@@ -486,6 +486,28 @@ class Daemon {
486
486
  runSharedOperation(name, signal, fn) {
487
487
  return this.operations.runShared(name, signal, fn);
488
488
  }
489
+ /**
490
+ * Compact + prune on behalf of a CLI caller.
491
+ *
492
+ * Lance's optimize runs with `deleteUnverified: true`, which its own docs say
493
+ * is only safe when no other process is working on the dataset. That held by
494
+ * accident, not by design: the daemon serializes its own maintenance, but
495
+ * `gmax doctor --fix` optimized in-process, so a user running it during a
496
+ * compaction was a second writer on a store that assumes one. Routing the
497
+ * request here makes it structural — with a daemon up, the daemon is the only
498
+ * optimizer, sharing the same serialization as the maintenance loop. Callers
499
+ * fall back to in-process only when no daemon answers, and are then the sole
500
+ * writer themselves.
501
+ */
502
+ runOptimize() {
503
+ return __awaiter(this, void 0, void 0, function* () {
504
+ const db = this.vectorDb;
505
+ if (!db)
506
+ throw new Error("daemon resources not ready");
507
+ yield this.runSharedOperation("store-maintenance", undefined, () => db.runMaintenance({ force: true }));
508
+ return { ok: true };
509
+ });
510
+ }
489
511
  operationStatus() {
490
512
  return this.operations.status;
491
513
  }
@@ -173,6 +173,9 @@ function handleCommand(daemon, cmd, conn) {
173
173
  diskPressure: daemon.getDiskPressure(),
174
174
  mlx: daemon.getMlxStatus(),
175
175
  };
176
+ case "optimize":
177
+ // Keeps compaction single-writer: see Daemon.runOptimize.
178
+ return yield daemon.runOptimize();
176
179
  case "project-stats": {
177
180
  const root = String(cmd.root || "");
178
181
  if (!root)
@@ -43,6 +43,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.VectorDB = exports.DiskPressureError = void 0;
46
+ exports.findStaleLanceTempFiles = findStaleLanceTempFiles;
47
+ exports.sweepStaleLanceTempFiles = sweepStaleLanceTempFiles;
46
48
  exports.isLanceCorruptionError = isLanceCorruptionError;
47
49
  exports.isMissingTableError = isMissingTableError;
48
50
  const fs = __importStar(require("node:fs"));
@@ -55,6 +57,73 @@ const filter_builder_1 = require("../utils/filter-builder");
55
57
  const logger_1 = require("../utils/logger");
56
58
  const ann_config_1 = require("./ann-config");
57
59
  const store_lease_1 = require("./store-lease");
60
+ /**
61
+ * Find abandoned Lance scratch files under `<lancedbDir>/ *.lance/data`.
62
+ *
63
+ * Lance stages a new fragment in a dot-prefixed `.tmp*` file and renames it into
64
+ * place on success. A killed or panicking optimize leaves the temp behind, and
65
+ * nothing ever collects it: it appears in no manifest, so `deleteUnverified`
66
+ * cannot prove it is garbage, and the leading dot hides it from `ls`. The result
67
+ * is permanent disk bloat that no amount of optimizing reclaims.
68
+ *
69
+ * Only files older than `minAgeMs` are reported. An in-flight write keeps
70
+ * bumping its temp file's mtime, so the age gate — not a writer count — is what
71
+ * makes deleting the result safe from any process.
72
+ */
73
+ function findStaleLanceTempFiles(lancedbDir, minAgeMs = config_1.STALE_TEMP_FILE_AGE_MS) {
74
+ const found = [];
75
+ const cutoff = Date.now() - minAgeMs;
76
+ let tables;
77
+ try {
78
+ tables = fs.readdirSync(lancedbDir);
79
+ }
80
+ catch (_a) {
81
+ return found;
82
+ }
83
+ for (const tableDir of tables) {
84
+ if (!tableDir.endsWith(".lance"))
85
+ continue;
86
+ const dataDir = `${lancedbDir}/${tableDir}/data`;
87
+ let entries;
88
+ try {
89
+ entries = fs.readdirSync(dataDir);
90
+ }
91
+ catch (_b) {
92
+ continue;
93
+ }
94
+ for (const entry of entries) {
95
+ if (!entry.startsWith(".tmp"))
96
+ continue;
97
+ const full = `${dataDir}/${entry}`;
98
+ try {
99
+ const s = fs.statSync(full);
100
+ if (!s.isFile() || s.mtimeMs > cutoff)
101
+ continue;
102
+ found.push({ path: full, size: s.size, mtimeMs: s.mtimeMs });
103
+ }
104
+ catch (_c) { }
105
+ }
106
+ }
107
+ return found;
108
+ }
109
+ /**
110
+ * Delete abandoned Lance scratch files. Returns bytes actually reclaimed —
111
+ * a file that vanishes between the scan and the unlink is not counted, so the
112
+ * number reported is never larger than what was freed.
113
+ */
114
+ function sweepStaleLanceTempFiles(lancedbDir, minAgeMs = config_1.STALE_TEMP_FILE_AGE_MS) {
115
+ let filesRemoved = 0;
116
+ let bytesFreed = 0;
117
+ for (const f of findStaleLanceTempFiles(lancedbDir, minAgeMs)) {
118
+ try {
119
+ fs.unlinkSync(f.path);
120
+ filesRemoved++;
121
+ bytesFreed += f.size;
122
+ }
123
+ catch (_a) { }
124
+ }
125
+ return { filesRemoved, bytesFreed };
126
+ }
58
127
  class DiskPressureError extends Error {
59
128
  constructor(message = "Disk critically low — writes suspended") {
60
129
  super(message);
@@ -956,6 +1025,15 @@ class VectorDB {
956
1025
  yield this.drainWrites();
957
1026
  try {
958
1027
  const done = (0, logger_1.timer)("vectordb", "optimize");
1028
+ // deleteUnverified deletes files Lance cannot prove are unreferenced.
1029
+ // LanceDB's own docs: only safe "if you can guarantee that no other
1030
+ // process is currently working on this dataset. Otherwise the dataset
1031
+ // could be put into a corrupted state." gmax is multi-process, so that
1032
+ // guarantee comes from routing compaction to a single writer — the
1033
+ // daemon serializes it (Daemon.runOptimize + the maintenance loop),
1034
+ // and CLI callers go through it rather than optimizing alongside it.
1035
+ // Anything new that calls optimize() must preserve that: either be the
1036
+ // daemon, or be the only process on the store.
959
1037
  const stats = yield table.optimize({
960
1038
  cleanupOlderThan: cutoff,
961
1039
  deleteUnverified: true,
@@ -1084,6 +1162,15 @@ class VectorDB {
1084
1162
  if (this.lastOptimizeDidWork) {
1085
1163
  const table = yield this.openExistingTableUnsafe();
1086
1164
  if (table) {
1165
+ // Collect abandoned scratch files before measuring. They are pure
1166
+ // disk with no manifest entry, so they inflate the ratio while being
1167
+ // invisible to optimize — measuring first would blame the compactor
1168
+ // for bytes it has no way to reach and send us into a second
1169
+ // full-table pass that reclaims nothing.
1170
+ const swept = sweepStaleLanceTempFiles(this.lancedbDir);
1171
+ if (swept.filesRemoved > 0) {
1172
+ (0, logger_1.log)("vectordb", `Swept ${swept.filesRemoved} abandoned temp file(s), freed ${(swept.bytesFreed / 1024 / 1024).toFixed(1)}MB`);
1173
+ }
1087
1174
  const stats = yield table.stats();
1088
1175
  const diskSize = this.getDirectorySize(this.lancedbDir);
1089
1176
  const logicalSize = stats.totalBytes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.11",
3
+ "version": "0.26.13",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.11",
3
+ "version": "0.26.13",
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",