grepmax 0.26.10 → 0.26.12

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.10",
13
+ "version": "0.26.12",
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;
@@ -281,7 +281,7 @@ exports.doctor = new commander_1.Command("doctor")
281
281
  ? "LOW"
282
282
  : "ok";
283
283
  }
284
- catch (_o) { }
284
+ catch (_p) { }
285
285
  const staleChunkerProjects = projects.filter((p) => {
286
286
  var _a;
287
287
  return p.status === "indexed" &&
@@ -486,7 +486,7 @@ exports.doctor = new commander_1.Command("doctor")
486
486
  yield mc.close();
487
487
  }
488
488
  }
489
- catch (_p) { }
489
+ catch (_q) { }
490
490
  }
491
491
  }
492
492
  // --fix auto-remediation
@@ -503,9 +503,39 @@ exports.doctor = new commander_1.Command("doctor")
503
503
  if (needsOptimize) {
504
504
  if (!opts.agent)
505
505
  console.log("... Running optimize (compact + prune)...");
506
- yield db.optimize(3, 0);
507
- if (!opts.agent)
508
- console.log("ok Optimize complete");
506
+ // Prefer the daemon. Lance's optimize deletes unverified files, which
507
+ // is only safe with a single writer on the store — optimizing here
508
+ // while the daemon compacts made this command a second writer. With a
509
+ // daemon up it owns compaction; without one we are the sole process
510
+ // and can do it in-process safely.
511
+ const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
512
+ const resp = yield sendDaemonCommand({ cmd: "optimize" }, { timeoutMs: 10 * 60 * 1000 });
513
+ let via = "daemon";
514
+ if (!resp.ok) {
515
+ // Only fall back when the daemon could not take it. A daemon that
516
+ // answered with a real failure should surface, not be retried here
517
+ // as a competing writer.
518
+ const reason = String((_k = resp.error) !== null && _k !== void 0 ? _k : "");
519
+ // Fall back ONLY when nothing is listening. A daemon that is merely
520
+ // too old ("unknown command") or busy ("timeout") is still a live
521
+ // writer on the store, so optimizing here would recreate the
522
+ // two-writer case this routing exists to prevent.
523
+ const daemonAbsent = reason.includes("ECONNREFUSED") || reason.includes("ENOENT");
524
+ if (daemonAbsent) {
525
+ yield db.optimize(3, 0);
526
+ via = "in-process (no daemon)";
527
+ }
528
+ else if (reason.includes("unknown command")) {
529
+ via = "";
530
+ console.log("WARN Optimize skipped: the running daemon predates the optimize command. Restart it (gmax watch --daemon -b) and re-run.");
531
+ }
532
+ else {
533
+ via = "";
534
+ console.log(`WARN Optimize failed via daemon: ${reason}`);
535
+ }
536
+ }
537
+ if (via && !opts.agent)
538
+ console.log(`ok Optimize complete (${via})`);
509
539
  fixed++;
510
540
  }
511
541
  if (orphanedProjects.length > 0) {
@@ -533,7 +563,7 @@ exports.doctor = new commander_1.Command("doctor")
533
563
  const done = yield sendStreamingCommand({ cmd: "index", root: p.root, reset: true }, () => { }).catch((e) => ({ ok: false, error: String(e) }));
534
564
  if (done.ok &&
535
565
  (!("degraded" in done) || done.degraded !== true)) {
536
- const chunks = (_k = done.indexed) !== null && _k !== void 0 ? _k : 0;
566
+ const chunks = (_l = done.indexed) !== null && _l !== void 0 ? _l : 0;
537
567
  if (opts.agent) {
538
568
  console.log(`stale_chunker_reindexed\tname=${name}\tchunks=${chunks}`);
539
569
  }
@@ -545,7 +575,7 @@ exports.doctor = new commander_1.Command("doctor")
545
575
  else if (!opts.agent) {
546
576
  const degraded = "degraded" in done && done.degraded === true;
547
577
  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`
578
+ ? `${(_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
579
  : done.error;
550
580
  console.log(`FAIL ${name}: reindex failed (${reason})`);
551
581
  }
@@ -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
  }
@@ -635,7 +657,7 @@ class Daemon {
635
657
  * on every search to annotate partial-result responses (Phase 6).
636
658
  */
637
659
  indexState(root) {
638
- var _a, _b, _c;
660
+ var _a, _b, _c, _d;
639
661
  const processor = this.processors.get(root);
640
662
  const batchPending = (_a = processor === null || processor === void 0 ? void 0 : processor.progress.pendingFiles) !== null && _a !== void 0 ? _a : 0;
641
663
  const processing = (_b = processor === null || processor === void 0 ? void 0 : processor.progress.processing) !== null && _b !== void 0 ? _b : false;
@@ -648,9 +670,23 @@ class Daemon {
648
670
  if (fullIdx && fullIdx.total > 0) {
649
671
  pendingFiles = Math.max(pendingFiles, fullIdx.total - fullIdx.processed);
650
672
  }
673
+ // Distinguish a real backlog from a queue draining as cache hits. A full
674
+ // index genuinely has work outstanding; a post-FSEvents-drop catchup mostly
675
+ // re-verifies unchanged files. Only claim "verifying" with a real sample and
676
+ // no initial-index work pending.
677
+ const { recentFiles, recentReindexed } = (_d = processor === null || processor === void 0 ? void 0 : processor.progress) !== null && _d !== void 0 ? _d : {
678
+ recentFiles: 0,
679
+ recentReindexed: 0,
680
+ };
681
+ const verifying = !fullIdx &&
682
+ !initialPending &&
683
+ recentFiles >= 50 &&
684
+ recentReindexed === 0 &&
685
+ pendingFiles > 0;
651
686
  return {
652
687
  indexing: !!fullIdx || processing || batchPending > 0 || initialPending,
653
688
  pendingFiles,
689
+ verifying,
654
690
  };
655
691
  }
656
692
  search(payload, signal) {
@@ -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)
@@ -76,6 +76,8 @@ class ProjectBatchProcessor {
76
76
  this.currentBatchAc = null;
77
77
  this.lastCorruptionLogMs = 0;
78
78
  this.policyChangedDuringBatch = false;
79
+ /** Rolling window of recent batch outcomes — see `progress`. */
80
+ this.recentBatches = [];
79
81
  this.projectRoot = opts.projectRoot;
80
82
  this.vectorDb = opts.vectorDb;
81
83
  this.metaCache = opts.metaCache;
@@ -129,12 +131,27 @@ class ProjectBatchProcessor {
129
131
  (_b = this.onActivity) === null || _b === void 0 ? void 0 : _b.call(this);
130
132
  this.scheduleBatch();
131
133
  }
132
- /** Live (re)index progress: files queued + whether a batch is running. */
134
+ /** Live (re)index progress: files queued + whether a batch is running.
135
+ *
136
+ * recentFiles/recentReindexed summarize the last few batches so callers can
137
+ * tell a real backlog from a queue draining as cache hits. A catchup after an
138
+ * FSEvents drop queues the whole project — on a large repo that is 10k+ files
139
+ * clearing at ~0.0s per 50-file batch with 0 reindexed, which is verification,
140
+ * not pending work. Without this, a large pendingFiles alone reads as "the
141
+ * index is far behind" and search callers caveat correct results. */
133
142
  get progress() {
143
+ let recentFiles = 0;
144
+ let recentReindexed = 0;
145
+ for (const b of this.recentBatches) {
146
+ recentFiles += b.files;
147
+ recentReindexed += b.reindexed;
148
+ }
134
149
  return {
135
150
  pendingFiles: this.pending.size,
136
151
  processing: this.processing,
137
152
  failedFiles: this.terminalFailures.size,
153
+ recentFiles,
154
+ recentReindexed,
138
155
  };
139
156
  }
140
157
  close() {
@@ -484,6 +501,10 @@ class ProjectBatchProcessor {
484
501
  if (reindexed > 0) {
485
502
  (_b = this.onReindex) === null || _b === void 0 ? void 0 : _b.call(this, reindexed, duration);
486
503
  }
504
+ this.recentBatches.push({ files: batch.size, reindexed });
505
+ if (this.recentBatches.length > ProjectBatchProcessor.RECENT_BATCH_WINDOW) {
506
+ this.recentBatches.shift();
507
+ }
487
508
  const remaining = this.pending.size;
488
509
  (0, logger_1.log)(this.wtag, `Batch complete: ${batch.size} files, ${reindexed} reindexed (${(duration / 1000).toFixed(1)}s)${remaining > 0 ? ` — ${remaining} remaining` : ""}`);
489
510
  for (const absPath of completed) {
@@ -609,3 +630,4 @@ class ProjectBatchProcessor {
609
630
  }
610
631
  }
611
632
  exports.ProjectBatchProcessor = ProjectBatchProcessor;
633
+ ProjectBatchProcessor.RECENT_BATCH_WINDOW = 5;
@@ -16,6 +16,16 @@ exports.formatIndexStateFooter = formatIndexStateFooter;
16
16
  function formatIndexStateFooter(state, opts) {
17
17
  if (!(state === null || state === void 0 ? void 0 : state.indexing))
18
18
  return null;
19
+ // Re-verification is not incompleteness. Saying "results may be incomplete"
20
+ // while a catchup re-checks thousands of unchanged files trains agents to
21
+ // distrust and retry correct answers, which is worse than saying nothing.
22
+ if (state.verifying) {
23
+ const n = state.pendingFiles;
24
+ if (opts.agent) {
25
+ return `[index: verifying ~${n} unchanged files · results current]`;
26
+ }
27
+ return `Index verifying ${n} unchanged file${n === 1 ? "" : "s"} — results are current.`;
28
+ }
19
29
  const count = state.pendingFiles > 0 ? `~${state.pendingFiles} files pending` : null;
20
30
  if (opts.agent) {
21
31
  const parts = ["index: syncing"];
@@ -956,6 +956,15 @@ class VectorDB {
956
956
  yield this.drainWrites();
957
957
  try {
958
958
  const done = (0, logger_1.timer)("vectordb", "optimize");
959
+ // deleteUnverified deletes files Lance cannot prove are unreferenced.
960
+ // LanceDB's own docs: only safe "if you can guarantee that no other
961
+ // process is currently working on this dataset. Otherwise the dataset
962
+ // could be put into a corrupted state." gmax is multi-process, so that
963
+ // guarantee comes from routing compaction to a single writer — the
964
+ // daemon serializes it (Daemon.runOptimize + the maintenance loop),
965
+ // and CLI callers go through it rather than optimizing alongside it.
966
+ // Anything new that calls optimize() must preserve that: either be the
967
+ // daemon, or be the only process on the store.
959
968
  const stats = yield table.optimize({
960
969
  cleanupOlderThan: cutoff,
961
970
  deleteUnverified: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.10",
3
+ "version": "0.26.12",
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.10",
3
+ "version": "0.26.12",
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",