grepmax 0.26.7 → 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.7",
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.7",
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.7",
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",
@@ -49,11 +49,26 @@ gmax skeleton src/lib/auth.ts
49
49
  gmax project
50
50
  ```
51
51
 
52
+ **Dependency shape** — god nodes, hub files, cycles, dead candidates in one pass:
53
+ ```
54
+ gmax audit --agent --top 10
55
+ ```
56
+ Start here for "explain this codebase" and "what's central here" questions. Rows are
57
+ `god <symbol> <path:line> <refs> <chunks>`, `hub <file> <refs> <deps>`, `cycle <files> <n>`,
58
+ `dead <symbol> <path:line>`. Dead candidates are hypotheses — dynamic dispatch and reflection
59
+ are invisible to the static graph.
60
+
52
61
  **Related files** — dependencies and dependents:
53
62
  ```
54
63
  gmax related src/lib/auth.ts
55
64
  ```
56
65
 
66
+ **Blast radius / test coverage** — before reporting on the risk of a change:
67
+ ```
68
+ gmax impact handleAuth --agent # dependents + affected tests
69
+ gmax test handleAuth --agent # tests exercising the symbol
70
+ ```
71
+
57
72
  ### Secondary: Standard tools
58
73
 
59
74
  - `Grep` — for exact string matches (identifiers, error codes, imports)
@@ -62,13 +77,18 @@ gmax related src/lib/auth.ts
62
77
 
63
78
  ## Strategy
64
79
 
80
+ 0. **Never start a local model.** `gmax investigate`, `review`, `summarize`, and `llm start` load a
81
+ multi-GB LLM and can stall the machine. They are not part of your toolkit — the index answers
82
+ these questions without them.
65
83
  1. **Start semantic** — use `gmax "query" --agent` first. Be specific (5+ words).
66
84
  2. **Peek before reading** — `gmax peek <symbol>` gives you signature + context in one call. Only `Read` the file if you need more.
67
85
  3. **Skeleton before reading** — `gmax skeleton <path>` before reading large files.
68
86
  4. **Trace for flow** — `gmax trace <symbol>` to understand call chains.
69
87
  5. **Use `--role ORCHESTRATION`** to skip type definitions and find actual logic.
70
88
  6. **Use `--agent` on everything** — compact output saves tokens.
71
- 7. **Parallelize** — fire multiple gmax/grep/read calls in parallel when independent.
89
+ 7. **Scope big repos** — `--in <subpath>` / `--exclude <subpath>` (repeatable) on audit, dead, log,
90
+ surprises, and search.
91
+ 8. **Parallelize** — fire multiple gmax/grep/read calls in parallel when independent.
72
92
 
73
93
  ## If gmax search returns nothing
74
94
 
@@ -9,6 +9,7 @@ allowed-tools: "Bash(gmax:*), Read"
9
9
  - **Know the exact string/symbol?** → `Grep` tool (fastest, zero overhead)
10
10
  - **Know the file already?** → `Read` tool directly
11
11
  - **Searching by concept/behavior?** → `Bash(gmax "query" --agent)` (semantic search)
12
+ - **New to the codebase?** → `Bash(gmax audit --agent)` (god nodes, hubs, cycles in one pass)
12
13
  - **Need full function body?** → `Bash(gmax extract <symbol>)` (complete source with line numbers)
13
14
  - **Quick symbol overview?** → `Bash(gmax peek <symbol>)` (signature + callers + callees)
14
15
  - **Need file structure?** → `Bash(gmax skeleton <path>)`
@@ -186,6 +187,25 @@ gmax dead handleAuth --in src/ # restrict to a sub-path
186
187
  ```
187
188
  Status is `DEAD` (no callers, not exported), `PUBLIC EXPORT` (no internal callers but the defining chunk is exported — check external usage), or `LIVE` (with caller count + top-3 file:line). The call graph reflects what tree-sitter chunked: dynamic dispatch, reflection, eval, and string-built call sites won't show up — `DEAD` is a hypothesis, not a proof.
188
189
 
190
+ ### Audit — `gmax audit`
191
+ ```
192
+ gmax audit # god nodes, hub files, cycles, dead candidates
193
+ gmax audit --agent --top 5 # compact TSV, 5 per category
194
+ gmax audit --in src/ --exclude tests/ # scope to a sub-path
195
+ ```
196
+ One pass over the static reference graph — the fastest way to get the shape of an unfamiliar
197
+ codebase. Agent rows are `god <symbol> <path:line> <refs> <chunks>`, `hub <file> <refs> <deps>`,
198
+ `cycle <comma-separated files> <n>`, `dead <symbol> <path:line>`, plus `scanned`/`dead_total`.
199
+ Dynamic dispatch, reflection, and eval are invisible to it, so dead candidates are hypotheses.
200
+
201
+ ### Surprises — `gmax surprises` (experimental)
202
+ ```
203
+ gmax surprises --agent # embedding-similar file pairs the static graph misses
204
+ gmax surprises --top 10 --min-sim 0.7 # tighten the similarity floor
205
+ ```
206
+ Orientation only — surfaces cross-directory pairs that look related by embedding but share no
207
+ static edge. Useful for spotting duplication or missing abstractions, not for proving anything.
208
+
189
209
  ### Context — `gmax context <topic-or-path> --budget <tokens>`
190
210
  ```
191
211
  gmax context "authentication system" --budget 4000
@@ -195,7 +215,16 @@ gmax context src/lib/auth/ --budget 3000
195
215
  ```
196
216
  Use the path form when you already know the file or directory; it skips semantic search and gives deterministic structure/excerpt context.
197
217
 
198
- ### Investigate — `gmax investigate "question"` (requires LLM)
218
+ ### Investigate — `gmax investigate "question"` (loads a multi-GB local LLM)
219
+
220
+ > **Ask the user first.** `investigate`, `review`, and `summarize` start `llama-server` with a
221
+ > multi-GB local model. On memory-constrained machines that can stall or hang the host. Never run
222
+ > them — or `gmax llm start` — on your own initiative, including "just to check" or as a fallback
223
+ > when search seems insufficient. Run them only when the user asks for that command in the moment.
224
+ >
225
+ > You almost never need them: `gmax context`, `--context-for-llm`, and `trace`/`impact` answer the
226
+ > same questions using the index you already have, with no model load.
227
+
199
228
  ```
200
229
  gmax investigate "how does authentication work?"
201
230
  gmax investigate "what would break if I changed VectorDB?" -v
@@ -210,16 +239,19 @@ gmax status --agent # compact: name\tchunks\tage\tstatus
210
239
  gmax related src/file.ts --agent # compact: dep:/rev: path\tcount
211
240
  gmax project --agent # compact: key\tvalue pairs
212
241
  gmax index # reindex current directory
242
+ gmax help-agent # reprint this cheatsheet in the terminal
213
243
  gmax config # view/change settings
214
244
  gmax doctor # health check
215
245
  gmax doctor --fix # auto-repair (compact, prune, clear stale locks)
216
- gmax llm on/off/start/stop/status # manage local LLM server
246
+ gmax repair --rebuild # last resort: destructive whole-store rebuild
247
+ gmax llm on/off/start/stop/status # local LLM server — see the warning above; ask first
217
248
  ```
218
249
 
219
250
  ## Workflow
220
251
 
221
252
  1. **Add** — `Bash(gmax add)` to register and index a new project
222
- 2. **Explore** — `Bash(gmax project)` for overview of a new codebase
253
+ 2. **Explore** — `Bash(gmax project)` for overview of a new codebase, then
254
+ `Bash(gmax audit --agent)` for the dependency shape (god nodes, hubs, cycles)
223
255
  3. **Search** — `Bash(gmax "query" --agent)` to find code. Add `--symbol` for function/class names.
224
256
  4. **Peek** — `Bash(gmax peek <symbol>)` for a quick overview (signature + callers + callees)
225
257
  5. **Extract** — `Bash(gmax extract <symbol>)` for the full function body with line numbers
@@ -244,6 +276,8 @@ gmax llm on/off/start/stop/status # manage local LLM server
244
276
  - **Use `--skeleton`** when `--agent` results aren't enough — shows file structure for top matches.
245
277
  - **Use `--imports`** when you need to understand a file's dependencies.
246
278
  - **Use `--root <dir>`** to search/trace/query a different project from your current directory.
279
+ - **Scope with `--in <subpath>` / `--exclude <subpath>`** (both repeatable) on `audit`, `surprises`,
280
+ `dead`, `log`, and search — narrows a big repo to the area you care about.
247
281
  - **Don't search for exact strings** — use grep/Grep for that. gmax finds concepts.
248
282
 
249
283
  ## If search fails or returns nothing