grepmax 0.26.12 → 0.26.14

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.12",
13
+ "version": "0.26.14",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -56,6 +56,7 @@ const lock_1 = require("../lib/utils/lock");
56
56
  const mlx_hf_cache_1 = require("../lib/utils/mlx-hf-cache");
57
57
  const project_registry_1 = require("../lib/utils/project-registry");
58
58
  const project_root_1 = require("../lib/utils/project-root");
59
+ const root_availability_1 = require("../lib/utils/root-availability");
59
60
  function formatSize(bytes) {
60
61
  if (bytes < 1024)
61
62
  return `${bytes} B`;
@@ -197,8 +198,9 @@ exports.doctor = new commander_1.Command("doctor")
197
198
  let needsOptimize = false;
198
199
  let staleLock = false;
199
200
  const orphanedProjects = [];
201
+ const unavailableProjects = [];
200
202
  try {
201
- const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
203
+ const { VectorDB, findStaleLanceTempFiles, sweepStaleLanceTempFiles } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
202
204
  const db = new VectorDB(config_1.PATHS.lancedbDir);
203
205
  const table = yield db.ensureTable();
204
206
  const totalChunks = yield table.countRows();
@@ -250,15 +252,33 @@ exports.doctor = new commander_1.Command("doctor")
250
252
  // Daemon status
251
253
  const { isDaemonRunning } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
252
254
  const daemonUp = yield isDaemonRunning();
253
- // Project registry health
255
+ // Project registry health.
256
+ //
257
+ // A root can be unreadable for two very different reasons, and only one of
258
+ // them is the project's fault. An unplugged external drive or a downed
259
+ // network share leaves the project perfectly intact — deregistering it
260
+ // would strand its vectors in the shared table with no owning project and
261
+ // would not come back when the volume returns. Only a root whose volume is
262
+ // mounted, and whose directory is still gone, is actually orphaned.
254
263
  const projects = (0, project_registry_1.listProjects)();
255
264
  for (const p of projects) {
256
- if (!fs.existsSync(p.root)) {
265
+ const availability = (0, root_availability_1.classifyRoot)(p.root);
266
+ if (availability === "missing")
257
267
  orphanedProjects.push(p.root);
258
- }
268
+ else if (availability === "unavailable")
269
+ unavailableProjects.push(p.root);
259
270
  }
260
271
  // Compute warning flags
261
- const bloatRatio = logicalSize > 0 ? diskSize / logicalSize : 0;
272
+ //
273
+ // Abandoned `.tmp*` scratch files are disk that optimize provably cannot
274
+ // reclaim (no manifest entry, so `deleteUnverified` will not touch them).
275
+ // They get their own remediation — a sweep — and are excluded from the
276
+ // ratio that decides whether to run a whole-table optimize, because
277
+ // counting them there prescribes a slow rewrite for bytes it cannot free.
278
+ const staleTempFiles = findStaleLanceTempFiles(config_1.PATHS.lancedbDir);
279
+ const staleTempBytes = staleTempFiles.reduce((n, f) => n + f.size, 0);
280
+ const reclaimableDiskSize = Math.max(0, diskSize - staleTempBytes);
281
+ const bloatRatio = logicalSize > 0 ? reclaimableDiskSize / logicalSize : 0;
262
282
  if (bloatRatio > 2.0)
263
283
  needsOptimize = true;
264
284
  // Track the compactor's own threshold. Warning (and worse, --fix forcing a
@@ -304,6 +324,9 @@ exports.doctor = new commander_1.Command("doctor")
304
324
  `lock=${lockStatus.split(" ")[0]}`,
305
325
  `daemon=${daemonUp ? "running" : "stopped"}`,
306
326
  `orphaned=${orphanedProjects.length}`,
327
+ `offline=${unavailableProjects.length}`,
328
+ `stale_temp=${staleTempFiles.length}`,
329
+ `stale_temp_bytes=${staleTempBytes}`,
307
330
  `stale_chunker=${staleChunkerProjects.length}`,
308
331
  `stale_embedding=${staleEmbeddingProjects.length}`,
309
332
  `legacy_embedding=${legacyEmbeddingCount}`,
@@ -380,11 +403,15 @@ exports.doctor = new commander_1.Command("doctor")
380
403
  console.log(`ok Disk: ${formatSize(availBytes)} available`);
381
404
  }
382
405
  // Storage
406
+ const storageLine = `${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk`;
383
407
  if (bloatRatio > 2.0) {
384
- console.log(`WARN Storage: ${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk (${bloatRatio.toFixed(1)}x — orphaned files)`);
408
+ console.log(`WARN Storage: ${storageLine} (${bloatRatio.toFixed(1)}x — orphaned files)`);
385
409
  }
386
410
  else {
387
- console.log(`ok Storage: ${totalChunks.toLocaleString()} rows, ${formatSize(logicalSize)} logical, ${formatSize(diskSize)} disk`);
411
+ console.log(`ok Storage: ${storageLine}`);
412
+ }
413
+ if (staleTempFiles.length > 0) {
414
+ console.log(`WARN Abandoned temp files: ${staleTempFiles.length} (${formatSize(staleTempBytes)}) — leftover from interrupted compaction, run with --fix`);
388
415
  }
389
416
  // Fragments
390
417
  if (numSmallFragments > config_1.FRAGMENT_COMPACT_THRESHOLD) {
@@ -440,13 +467,19 @@ exports.doctor = new commander_1.Command("doctor")
440
467
  if (legacyNotice)
441
468
  console.log(legacyNotice);
442
469
  // Projects
470
+ if (unavailableProjects.length > 0) {
471
+ console.log(`INFO Offline projects: ${unavailableProjects.length} (volume not mounted — kept, reconnect to resume)`);
472
+ for (const up of unavailableProjects) {
473
+ console.log(` - ${up}`);
474
+ }
475
+ }
443
476
  if (orphanedProjects.length > 0) {
444
477
  console.log(`WARN Orphaned projects: ${orphanedProjects.length} (directories no longer exist)`);
445
478
  for (const op of orphanedProjects) {
446
479
  console.log(` - ${op}`);
447
480
  }
448
481
  }
449
- else if (projects.length > 0) {
482
+ else if (projects.length > 0 && unavailableProjects.length === 0) {
450
483
  console.log(`ok Projects: ${projects.length} registered, all directories exist`);
451
484
  }
452
485
  // Cache Coherence
@@ -500,6 +533,16 @@ exports.doctor = new commander_1.Command("doctor")
500
533
  console.log("ok Removed stale lock");
501
534
  fixed++;
502
535
  }
536
+ // Sweep before optimize: these bytes are unreachable by optimize, and
537
+ // clearing them first means any bloat still visible afterwards is real.
538
+ // Unlike optimize this needs no single-writer routing — the age gate in
539
+ // findStaleLanceTempFiles already excludes anything a writer could hold.
540
+ if (staleTempFiles.length > 0) {
541
+ const swept = sweepStaleLanceTempFiles(config_1.PATHS.lancedbDir);
542
+ if (!opts.agent)
543
+ console.log(`ok Removed ${swept.filesRemoved} abandoned temp file(s), freed ${formatSize(swept.bytesFreed)}`);
544
+ fixed++;
545
+ }
503
546
  if (needsOptimize) {
504
547
  if (!opts.agent)
505
548
  console.log("... Running optimize (compact + prune)...");
@@ -538,6 +581,9 @@ exports.doctor = new commander_1.Command("doctor")
538
581
  console.log(`ok Optimize complete (${via})`);
539
582
  fixed++;
540
583
  }
584
+ // Only roots whose volume is mounted and whose directory is still gone
585
+ // reach this list — projects on an unplugged drive are classified
586
+ // `unavailable` and never enter it, so --fix cannot deregister them.
541
587
  if (orphanedProjects.length > 0) {
542
588
  for (const op of orphanedProjects) {
543
589
  (0, project_registry_1.removeProject)(op);
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",
@@ -67,6 +67,7 @@ const logger_1 = require("../utils/logger");
67
67
  const operation_coordinator_1 = require("../utils/operation-coordinator");
68
68
  const process_1 = require("../utils/process");
69
69
  const project_registry_1 = require("../utils/project-registry");
70
+ const root_availability_1 = require("../utils/root-availability");
70
71
  const watcher_store_1 = require("../utils/watcher-store");
71
72
  const pool_1 = require("../workers/pool");
72
73
  const ipc_handler_1 = require("./ipc-handler");
@@ -352,7 +353,10 @@ class Daemon {
352
353
  const indexed = allProjects.filter((p) => p.status === "indexed");
353
354
  for (const p of indexed) {
354
355
  if (!fs.existsSync(p.root)) {
355
- console.log(`[daemon] Skipping ${path.basename(p.root)} — directory not found`);
356
+ // Skipping is already the safe behavior — the project keeps its registry
357
+ // entry and its vectors either way. Naming the reason keeps an unplugged
358
+ // drive from reading like data loss in the log.
359
+ console.log(`[daemon] Skipping ${path.basename(p.root)} — ${(0, root_availability_1.describeRoot)(p.root)}`);
356
360
  continue;
357
361
  }
358
362
  try {
@@ -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);
@@ -1093,6 +1162,15 @@ class VectorDB {
1093
1162
  if (this.lastOptimizeDidWork) {
1094
1163
  const table = yield this.openExistingTableUnsafe();
1095
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
+ }
1096
1174
  const stats = yield table.stats();
1097
1175
  const diskSize = this.getDirectorySize(this.lancedbDir);
1098
1176
  const logicalSize = stats.totalBytes;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.classifyRoot = classifyRoot;
37
+ exports.isRootUnavailable = isRootUnavailable;
38
+ exports.describeRoot = describeRoot;
39
+ const fs = __importStar(require("node:fs"));
40
+ /**
41
+ * Directories that hold mount points rather than user data, with how many path
42
+ * segments below them the mount point itself sits.
43
+ *
44
+ * `mountDepth` errs high where a platform is ambiguous (Linux uses both
45
+ * `/media/<volume>` and `/media/<user>/<volume>`). Overshooting biases the
46
+ * classifier toward `unavailable`, which is the safe direction: a wrong
47
+ * `unavailable` only means we decline to auto-remove a genuinely dead project,
48
+ * while a wrong `missing` silently deregisters a live one.
49
+ */
50
+ const MOUNT_CONTAINERS = [
51
+ { prefix: "/Volumes", mountDepth: 1 }, // macOS
52
+ { prefix: "/run/media", mountDepth: 2 }, // Linux, udisks2
53
+ { prefix: "/media", mountDepth: 2 },
54
+ { prefix: "/mnt", mountDepth: 1 },
55
+ { prefix: "/net", mountDepth: 1 }, // autofs network mounts
56
+ ];
57
+ function exists(p) {
58
+ try {
59
+ fs.statSync(p);
60
+ return true;
61
+ }
62
+ catch (_a) {
63
+ // A disconnected network mount can throw EIO/ETIMEDOUT rather than ENOENT.
64
+ // Either way the path is not readable, and the caller's container check
65
+ // decides whether that means unavailable or missing.
66
+ return false;
67
+ }
68
+ }
69
+ /**
70
+ * Classify a project root that may live on removable or network storage.
71
+ *
72
+ * Longest matching container wins, so `/run/media` is not shadowed by `/media`.
73
+ */
74
+ function classifyRoot(root) {
75
+ if (exists(root))
76
+ return "present";
77
+ const container = MOUNT_CONTAINERS.filter((c) => root === c.prefix || root.startsWith(`${c.prefix}/`)).sort((a, b) => b.prefix.length - a.prefix.length)[0];
78
+ if (!container)
79
+ return "missing";
80
+ // The mount point is the container plus `mountDepth` segments. If it is
81
+ // absent, nothing is mounted there and the project is merely out of reach.
82
+ const rest = root.slice(container.prefix.length).split("/").filter(Boolean);
83
+ if (rest.length < container.mountDepth)
84
+ return "missing";
85
+ const mountPoint = `${container.prefix}/${rest.slice(0, container.mountDepth).join("/")}`;
86
+ return exists(mountPoint) ? "missing" : "unavailable";
87
+ }
88
+ /**
89
+ * True when the root is absent only because its volume is not mounted.
90
+ * Callers that delete or rewrite persistent state must check this first.
91
+ */
92
+ function isRootUnavailable(root) {
93
+ return classifyRoot(root) === "unavailable";
94
+ }
95
+ /** Human-readable reason for a non-present root, for logs and doctor output. */
96
+ function describeRoot(root) {
97
+ switch (classifyRoot(root)) {
98
+ case "present":
99
+ return "available";
100
+ case "unavailable":
101
+ return "volume not mounted";
102
+ case "missing":
103
+ return "directory not found";
104
+ }
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.12",
3
+ "version": "0.26.14",
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.12",
3
+ "version": "0.26.14",
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",