grepmax 0.26.8 → 0.26.9

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.8",
13
+ "version": "0.26.9",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
package/README.md CHANGED
@@ -221,6 +221,15 @@ The daemon auto-starts when you run `gmax add`, `gmax index`, `gmax remove`, `gm
221
221
 
222
222
  gmax can use a local LLM (via llama-server) for agentic codebase investigation. This is entirely opt-in and disabled by default — gmax works fine without it.
223
223
 
224
+ > **Memory footprint.** `gmax llm start`, `investigate`, `review`, and `summarize` load a multi-GB
225
+ > GGUF model (typical coding models run 16–21 GB). On a memory-constrained machine that can stall
226
+ > or hang the host. Start it deliberately, not as a reflex — and if you run coding agents against
227
+ > this repo, they should be told not to invoke these on their own initiative. The bundled skill
228
+ > already carries that instruction.
229
+ >
230
+ > Everything else in gmax — search, `trace`, `impact`, `context`, `--context-for-llm` — runs off
231
+ > the local index with no model load.
232
+
224
233
  ```bash
225
234
  gmax llm on # Enable LLM features (persists to config)
226
235
  gmax llm start # Start llama-server (auto-starts daemon too)
@@ -393,6 +402,19 @@ gmax watch stop && gmax watch --daemon -b # Restart daemon
393
402
 
394
403
  `gmax doctor` reports ANN index state. `ANN: vector index not built` is normal with the default exact-search configuration.
395
404
 
405
+ ### Known issues
406
+
407
+ Some log output looks alarming but is expected and self-healing. Before filing a bug, check
408
+ [`docs/known-limitations.md`](docs/known-limitations.md) — it covers what each case means and
409
+ whether you need to act.
410
+
411
+ | You see | What it means |
412
+ |---|---|
413
+ | `Optimize panicked ... inverted/builder.rs` | Upstream Lance bug in incremental FTS merge ([lance#8310](https://github.com/lance-format/lance/issues/8310)). gmax rebuilds the index and retries automatically; no data loss, no action needed. |
414
+ | `disabling auto-rebuild until an optimize succeeds` | Transient, not a wedge. The next successful optimize clears it. Search stays available. |
415
+ | `ANN: vector index not built` | Normal — exact search is the default. |
416
+ | `npm audit` reports advisories on install | Two transitive advisories (`sharp`, `adm-zip`) have no upstream fix yet; neither is on a code path gmax executes. |
417
+
396
418
  ## Contributing
397
419
 
398
420
  See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture details.
package/dist/config.js CHANGED
@@ -251,8 +251,28 @@ exports.DISK_LOW_BYTES = (() => {
251
251
  const gb = Number.parseFloat((_a = process.env.GMAX_DISK_LOW_GB) !== null && _a !== void 0 ? _a : "20");
252
252
  return (Number.isFinite(gb) && gb > 0 ? gb : 20) * 1024 * 1024 * 1024;
253
253
  })();
254
- // Trigger compaction when small (uncompacted) fragment count exceeds this
255
- exports.FRAGMENT_COMPACT_THRESHOLD = 50;
254
+ // Trigger compaction when small (uncompacted) fragment count exceeds this.
255
+ //
256
+ // Deliberately high. Lance's optimize() merges the *whole* table into ~2
257
+ // fragments regardless of how many small ones it absorbs, so a compaction costs
258
+ // the same full-table rewrite at 51 fragments as at 400 — the threshold only
259
+ // decides how far that fixed cost is amortized. At 50 it was amortized as little
260
+ // as possible: on a continuously-watched project this fired every ~15-25min and
261
+ // rewrote ~10-14GB each time, measured at 40 compactions / 586GB over 26h.
262
+ //
263
+ // Fragmentation also does not cost what you would expect. With ANN off (the
264
+ // default), vector search is a brute-force scan that Lance parallelizes across
265
+ // fragments, so compacting to 2 fragments collapses it to 2-way parallelism.
266
+ // Interleaved benchmark, 395k rows / 384d, 96 samples per config:
267
+ //
268
+ // fragments | 2 | 50 | 200 | 800
269
+ // vector p50| 165.6 | 73.4 | 61.6 | 66.8 (ms)
270
+ //
271
+ // Flat from 50-800; 2 is 2.5x slower. So raising this wins on writes *and*
272
+ // latency. 400 sits inside the measured range with margin at both ends.
273
+ // Caveat: measured on the no-ANN path — revisit if GMAX_ANN=1 becomes default,
274
+ // since an ANN index partitions independently of fragments.
275
+ exports.FRAGMENT_COMPACT_THRESHOLD = 400;
256
276
  // Extensions we consider for indexing to avoid binary noise and improve relevance.
257
277
  exports.INDEXABLE_EXTENSIONS = new Set([
258
278
  ".ts",
@@ -174,7 +174,11 @@ class Daemon {
174
174
  return new pool_1.WorkerPool(generation, embedMode);
175
175
  }
176
176
  createVectorDb(vectorDim, lease) {
177
- return new vector_db_1.VectorDB(config_1.PATHS.lancedbDir, vectorDim, lease);
177
+ const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir, vectorDim, lease);
178
+ // The daemon owns the store, so it is the one process allowed to create or
179
+ // rebuild the shared FTS index. Readers adopt it; see VectorDB.adoptFTSIndex.
180
+ db.markIndexOwner();
181
+ return db;
178
182
  }
179
183
  mlxMode(config) {
180
184
  if (config.embedMode !== "gpu")
@@ -318,6 +322,7 @@ class Daemon {
318
322
  this.activeConfig = (0, index_config_1.readGlobalConfig)();
319
323
  this.activeGeneration = (0, embedding_generation_1.resolveEmbeddingGeneration)(this.activeConfig);
320
324
  this.vectorDb = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir, this.activeGeneration.vectorDim);
325
+ this.vectorDb.markIndexOwner();
321
326
  this.workerPool = this.createWorkerPool(this.activeGeneration, this.activeConfig.embedMode);
322
327
  this.vectorDb.startMaintenanceLoop((fn) => this.runSharedOperation("store-maintenance", undefined, () => fn()));
323
328
  console.log("[daemon] Opening MetaCache:", config_1.PATHS.lmdbPath);
@@ -15,6 +15,7 @@ exports.buildWhereClause = buildWhereClause;
15
15
  const config_1 = require("../../config");
16
16
  const ann_config_1 = require("../store/ann-config");
17
17
  const filter_builder_1 = require("../utils/filter-builder");
18
+ const logger_1 = require("../utils/logger");
18
19
  const pool_1 = require("../workers/pool");
19
20
  const intent_1 = require("./intent");
20
21
  const pagerank_1 = require("./pagerank");
@@ -436,12 +437,15 @@ class Searcher {
436
437
  this.ftsIndexChecked = true;
437
438
  this.ftsLastCheckedAt = now;
438
439
  try {
439
- yield this.db.createFTSIndex();
440
+ // Adopt-only: search runs in every process, so building here raced
441
+ // CreateIndex across processes and duplicated a full-size index per
442
+ // winner. Owners build it (daemon pre-warm, `gmax index`, maintenance).
443
+ yield this.db.adoptFTSIndex();
440
444
  this.ftsAvailable = true;
441
445
  }
442
446
  catch (e) {
443
447
  this.ftsAvailable = false;
444
- console.warn("[Searcher] Failed to ensure FTS index:", e);
448
+ (0, logger_1.debug)("searcher", `FTS index unavailable, vector-only search: ${e}`);
445
449
  }
446
450
  }
447
451
  // Phase A: Lightweight retrieval — only columns needed for RRF, cosine, boost, dedup
@@ -501,13 +505,23 @@ class Searcher {
501
505
  this.ftsAvailable = false;
502
506
  const msg = e instanceof Error ? e.message : String(e);
503
507
  if (msg.includes("position")) {
504
- // FTS index lacks positional data — rebuild it
505
- try {
506
- yield this.db.createFTSIndex(true);
507
- this.ftsAvailable = true;
508
- console.warn("[Searcher] Rebuilt FTS index with position support — retry search");
508
+ // FTS index lacks positional data — rebuild it, but only from the
509
+ // index owner. This drops the index before recreating it, so a
510
+ // non-owner (CLI fallback, MCP session) would yank the shared index
511
+ // out from under every other process and, with several running at
512
+ // once, race CreateIndex on the way back up. Non-owners degrade to
513
+ // vector-only and let the daemon repair it.
514
+ if (this.db.canBuildIndexes()) {
515
+ try {
516
+ yield this.db.createFTSIndex(true);
517
+ this.ftsAvailable = true;
518
+ console.warn("[Searcher] Rebuilt FTS index with position support — retry search");
519
+ }
520
+ catch (_r) { }
521
+ }
522
+ else {
523
+ (0, logger_1.debug)("searcher", "FTS index lacks positions; deferring rebuild to the index owner");
509
524
  }
510
- catch (_r) { }
511
525
  }
512
526
  else {
513
527
  console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
@@ -102,6 +102,8 @@ class VectorDB {
102
102
  this.maintainedTableVersion = null;
103
103
  this.lastMaintenanceMs = 0;
104
104
  this.ftsIndexEnsured = false;
105
+ /** Only an index owner may create/rebuild shared indexes — see markIndexOwner. */
106
+ this.indexOwner = false;
105
107
  this.lastOptimizeDidWork = false;
106
108
  this.maintenanceRunner = (fn) => fn();
107
109
  /** Latched when a from-scratch FTS rebuild failed to stop an optimize
@@ -735,6 +737,48 @@ class VectorDB {
735
737
  return this.withWriteGate(() => this.createFTSIndexUnsafe(rebuild, retries));
736
738
  });
737
739
  }
740
+ /**
741
+ * Mark this instance as the store's index owner — the daemon, or a
742
+ * deliberately exclusive CLI operation. Only an owner may create or rebuild
743
+ * the shared FTS index; see `adoptFTSIndex`.
744
+ */
745
+ markIndexOwner() {
746
+ this.indexOwner = true;
747
+ }
748
+ /** Whether this instance may create or rebuild shared indexes. */
749
+ canBuildIndexes() {
750
+ return this.indexOwner;
751
+ }
752
+ /**
753
+ * Adopt the FTS index if one already exists, without ever building it.
754
+ *
755
+ * Search runs in *every* process — the daemon, each CLI invocation that falls
756
+ * back in-process, each MCP session. `createFTSIndex` on a miss meant N
757
+ * processes could issue `CreateIndex` against the same table version
758
+ * concurrently: Lance rejects the losers with a retryable commit conflict
759
+ * (observed 2026-08-17T00:03, all 5 retries lost) and every winner writes a
760
+ * full-size inverted index — ~1.4GB each on this store, which is how
761
+ * `_indices` reached 47GB across 35 near-identical copies.
762
+ *
763
+ * Building is therefore reserved for the explicit paths that already run
764
+ * under an owner: daemon pre-warm, `gmax index`, and post-optimize
765
+ * maintenance. Throws when no index exists so callers set `ftsAvailable =
766
+ * false` and degrade to vector-only search — never silently reports FTS as
767
+ * ready.
768
+ */
769
+ adoptFTSIndex() {
770
+ return __awaiter(this, void 0, void 0, function* () {
771
+ if (this.ftsIndexEnsured)
772
+ return;
773
+ const table = yield this.ensureTable();
774
+ const indices = yield table.listIndices();
775
+ const existing = indices.find((index) => index.name === "content_idx" || index.columns.includes("content"));
776
+ if (!existing) {
777
+ throw new Error("[vector-db] FTS index not built yet; this process is not the index owner");
778
+ }
779
+ this.ftsIndexEnsured = true;
780
+ });
781
+ }
738
782
  createVectorIndex() {
739
783
  return __awaiter(this, arguments, void 0, function* (rebuild = false, retries = 5, checkOnly = false) {
740
784
  if (this.checkDiskPressure() !== "ok") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.8",
3
+ "version": "0.26.9",
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.8",
3
+ "version": "0.26.9",
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",