grepmax 0.26.23 → 0.26.26

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.23",
13
+ "version": "0.26.26",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -62,8 +62,16 @@ const logger_1 = require("../utils/logger");
62
62
  const project_registry_1 = require("../utils/project-registry");
63
63
  const watcher_store_1 = require("../utils/watcher-store");
64
64
  // Watcher health windows used for FSEvents auto-recovery.
65
- const FSEVENTS_RECOVERY_INTERVAL_MS = 60 * 60 * 1000; // try recovery hourly
65
+ // Poll mode is the expensive state (a full catchup scan every 5 min), and in
66
+ // practice the first reattach attempt succeeds once the burst that caused the
67
+ // overflows has passed. Probe often; a failed probe just drops back to polling.
68
+ const FSEVENTS_RECOVERY_INTERVAL_MS = 15 * 60 * 1000;
66
69
  const FSEVENTS_HEALTH_WINDOW_MS = 5 * 60 * 1000; // 5 min of quiet = "healthy"
70
+ // Overflows arrive in bursts (a build, a checkout, a venv install). Every
71
+ // recovery inside this window folds into one deferred catchup scan instead of
72
+ // each rescanning the project (~25s on a 26k-file tree) or skipping outright
73
+ // and leaving its drop window uncovered.
74
+ const CATCHUP_COOLDOWN_MS = 5 * 60 * 1000;
67
75
  function abortableDelay(ms, signal) {
68
76
  if (signal.aborted)
69
77
  return Promise.resolve();
@@ -97,6 +105,7 @@ class WatcherManager {
97
105
  this.recoveryTimeouts = new Map();
98
106
  this.lastOverflowMs = new Map();
99
107
  this.lastCatchupEndMs = new Map();
108
+ this.deferredCatchups = new Map();
100
109
  this.catchups = new Map();
101
110
  this.degradedRoots = new Set();
102
111
  this.terminalFailures = new Map();
@@ -354,10 +363,12 @@ class WatcherManager {
354
363
  yield this.subscribeWatcher(root, processor);
355
364
  if (this.deps.processors.get(root) !== processor)
356
365
  return;
357
- const lastCatchup = (_a = this.lastCatchupEndMs.get(root)) !== null && _a !== void 0 ? _a : 0;
358
- const CATCHUP_COOLDOWN_MS = 60000;
359
- if (Date.now() - lastCatchup < CATCHUP_COOLDOWN_MS) {
360
- console.log(`[daemon:${name}] Skipping catchup scan (last completed ${Math.round((Date.now() - lastCatchup) / 1000)}s ago)`);
366
+ const sinceLastMs = Date.now() - ((_a = this.lastCatchupEndMs.get(root)) !== null && _a !== void 0 ? _a : 0);
367
+ if (sinceLastMs < CATCHUP_COOLDOWN_MS) {
368
+ const delayMs = CATCHUP_COOLDOWN_MS - sinceLastMs;
369
+ if (this.deferCatchup(root, processor, delayMs)) {
370
+ console.log(`[daemon:${name}] Deferring catchup scan (last completed ${Math.round(sinceLastMs / 1000)}s ago; one coalesced scan in ${Math.round(delayMs / 1000)}s)`);
371
+ }
361
372
  }
362
373
  else {
363
374
  yield this.runCatchup(root, processor);
@@ -374,6 +385,34 @@ class WatcherManager {
374
385
  }, delayMs);
375
386
  this.recoveryTimeouts.set(root, timeout);
376
387
  }
388
+ /**
389
+ * Schedule one catchup scan for the root at the end of the cooldown. Returns
390
+ * false when a deferred scan is already pending — every overflow inside the
391
+ * window is covered by that single scan, since it walks the whole project.
392
+ */
393
+ deferCatchup(root, processor, delayMs) {
394
+ if (this.deferredCatchups.has(root))
395
+ return false;
396
+ const name = path.basename(root);
397
+ const timer = setTimeout(() => {
398
+ this.deferredCatchups.delete(root);
399
+ if (this.deps.getShuttingDown() ||
400
+ this.deps.processors.get(root) !== processor)
401
+ return;
402
+ this.runCatchup(root, processor).catch((err) => {
403
+ console.error(`[daemon:${name}] Deferred catchup failed:`, err);
404
+ });
405
+ }, delayMs);
406
+ this.deferredCatchups.set(root, timer);
407
+ return true;
408
+ }
409
+ cancelDeferredCatchup(root) {
410
+ const timer = this.deferredCatchups.get(root);
411
+ if (!timer)
412
+ return;
413
+ clearTimeout(timer);
414
+ this.deferredCatchups.delete(root);
415
+ }
377
416
  /**
378
417
  * Once a project has fallen back to poll mode, periodically try to upgrade
379
418
  * back to native FSEvents. The buffer overflows that triggered the fallback
@@ -672,6 +711,7 @@ class WatcherManager {
672
711
  clearTimeout(recoveryTimeout);
673
712
  this.recoveryTimeouts.delete(root);
674
713
  }
714
+ this.cancelDeferredCatchup(root);
675
715
  this.pendingOps.delete(`recover:${root}`);
676
716
  const processor = this.deps.processors.get(root);
677
717
  this.deps.processors.delete(root);
@@ -744,6 +784,10 @@ class WatcherManager {
744
784
  for (const timeout of this.recoveryTimeouts.values()) {
745
785
  clearTimeout(timeout);
746
786
  }
787
+ for (const timeout of this.deferredCatchups.values()) {
788
+ clearTimeout(timeout);
789
+ }
790
+ this.deferredCatchups.clear();
747
791
  this.recoveryTimeouts.clear();
748
792
  const catchups = [...this.catchups.values()];
749
793
  for (const active of catchups)
@@ -123,6 +123,20 @@ class ProjectBatchProcessor {
123
123
  !this.metaCache.get(normalized) &&
124
124
  !(event === "unlink" && (options === null || options === void 0 ? void 0 : options.forceDelete)))
125
125
  return;
126
+ // A policy-ignored path that was never indexed has nothing to add and
127
+ // nothing to delete. Drop it here rather than after a debounce, a batch
128
+ // slot, and an lstat — transient lock/build files under a gitignored
129
+ // directory can arrive thousands of times a day. Anything with a meta
130
+ // entry still flows through so a policy change can retire its vectors,
131
+ // and forced repairs/deletes bypass the check because they target
132
+ // vectors the meta cache may not know about.
133
+ const isIgnored = this.filePolicy.isIgnoredByLoadedPolicy;
134
+ if (isIgnored &&
135
+ !(options === null || options === void 0 ? void 0 : options.forceDelete) &&
136
+ !(options === null || options === void 0 ? void 0 : options.forceReprocess) &&
137
+ !this.metaCache.get(normalized) &&
138
+ isIgnored.call(this.filePolicy, normalized))
139
+ return;
126
140
  if (options === null || options === void 0 ? void 0 : options.forceReprocess) {
127
141
  this.forcedReprocess.set(normalized, ++this.forceGeneration);
128
142
  }
@@ -66,6 +66,8 @@ class ProjectFilePolicy {
66
66
  constructor(projectRoot, options = {}) {
67
67
  var _a, _b;
68
68
  this.ignoreCache = new Map();
69
+ // Scopes whose ignore files have finished loading, for synchronous checks.
70
+ this.loadedScopes = new Map();
69
71
  this.projectRoot = path.resolve(projectRoot);
70
72
  try {
71
73
  this.canonicalRoot = fs.realpathSync(this.projectRoot);
@@ -88,6 +90,36 @@ class ProjectFilePolicy {
88
90
  }
89
91
  invalidateIgnoreCache() {
90
92
  this.ignoreCache.clear();
93
+ this.loadedScopes.clear();
94
+ }
95
+ /**
96
+ * Synchronous, best-effort ignore check against policy already in memory:
97
+ * the default patterns plus every .gitignore/.gmaxignore scope an earlier
98
+ * classify has loaded. Never touches the filesystem, so it is safe on the
99
+ * watcher's event path. `false` only means "not provably ignored yet" —
100
+ * callers must still classify before indexing.
101
+ */
102
+ isIgnoredByLoadedPolicy(absPath) {
103
+ const resolved = path.resolve(absPath);
104
+ if (!this.isLexicallyContained(resolved))
105
+ return true;
106
+ const rootRelative = this.relative(resolved);
107
+ if (!rootRelative)
108
+ return false;
109
+ if (this.rootFilter.ignores(rootRelative))
110
+ return true;
111
+ for (const dir of this.scopeDirectories(path.dirname(resolved))) {
112
+ const scope = this.loadedScopes.get(dir);
113
+ if (!(scope === null || scope === void 0 ? void 0 : scope.filter))
114
+ continue;
115
+ const relative = path
116
+ .relative(scope.dir, resolved)
117
+ .split(path.sep)
118
+ .join("/");
119
+ if (relative && scope.filter.ignores(relative))
120
+ return true;
121
+ }
122
+ return false;
91
123
  }
92
124
  isLexicallyContained(absPath) {
93
125
  return (0, path_containment_1.isPathWithin)(this.projectRoot, path.resolve(absPath));
@@ -127,9 +159,14 @@ class ProjectFilePolicy {
127
159
  return { dir, filter };
128
160
  }))();
129
161
  this.ignoreCache.set(dir, pending);
130
- void pending.catch(() => {
131
- if (this.ignoreCache.get(dir) === pending)
162
+ void pending.then((scope) => {
163
+ if (this.ignoreCache.get(dir) === pending) {
164
+ this.loadedScopes.set(dir, scope);
165
+ }
166
+ }, () => {
167
+ if (this.ignoreCache.get(dir) === pending) {
132
168
  this.ignoreCache.delete(dir);
169
+ }
133
170
  });
134
171
  return pending;
135
172
  });
@@ -50,6 +50,8 @@ exports.DEFAULT_IGNORE_PATTERNS = [
50
50
  "node_modules",
51
51
  "dist",
52
52
  "build",
53
+ ".build", // SwiftPM
54
+ "DerivedData", // Xcode
53
55
  "out",
54
56
  "target",
55
57
  "__pycache__",
@@ -71,6 +71,8 @@ const IGNORED_DIR_NAMES = [
71
71
  ".gmax",
72
72
  "dist",
73
73
  "build",
74
+ ".build", // SwiftPM
75
+ "DerivedData", // Xcode
74
76
  "out",
75
77
  "target",
76
78
  "__pycache__",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.23",
3
+ "version": "0.26.26",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -77,37 +77,37 @@
77
77
  "license": "Apache-2.0",
78
78
  "description": "Semantic code search for coding agents. Local embeddings, LLM summaries, call graph tracing.",
79
79
  "dependencies": {
80
- "@clack/prompts": "^1.6.0",
80
+ "@clack/prompts": "^1.7.0",
81
81
  "@huggingface/transformers": "^4.2.0",
82
82
  "@lancedb/lancedb": "0.38.0-beta.3",
83
83
  "@modelcontextprotocol/sdk": "^1.30.0",
84
- "@parcel/watcher": "^2.5.6",
84
+ "@parcel/watcher": "^2.6.0",
85
85
  "apache-arrow": "^18.1.0",
86
86
  "chalk": "^5.6.2",
87
87
  "cli-highlight": "^2.1.11",
88
88
  "commander": "^15.0.0",
89
89
  "dotenv": "^17.4.2",
90
90
  "fast-glob": "^3.3.3",
91
- "ignore": "^7.0.5",
91
+ "ignore": "^7.0.8",
92
92
  "lmdb": "^3.5.6",
93
- "numkong": "^7.7.1",
93
+ "numkong": "^7.8.2",
94
94
  "onnxruntime-node": "1.24.3",
95
95
  "openai": "^6.45.0",
96
96
  "ora": "^9.4.1",
97
97
  "proper-lockfile": "^4.1.2",
98
- "uuid": "^14.0.1",
98
+ "uuid": "^14.0.2",
99
99
  "web-tree-sitter": "^0.26.9",
100
- "zod": "^4.4.3"
100
+ "zod": "^4.5.4"
101
101
  },
102
102
  "devDependencies": {
103
- "@biomejs/biome": "2.4.10",
104
- "@types/node": "^26.0.1",
103
+ "@biomejs/biome": "2.5.11",
104
+ "@types/node": "^26.4.1",
105
105
  "@types/proper-lockfile": "^4.1.4",
106
- "node-gyp": "^13.0.0",
106
+ "node-gyp": "^13.0.2",
107
107
  "ts-node": "^10.9.2",
108
- "tsx": "^4.22.4",
108
+ "tsx": "^4.23.13",
109
109
  "typescript": "^6.0.3",
110
- "vite": "^8.1.0",
111
- "vitest": "^4.1.9"
110
+ "vite": "^8.2.2",
111
+ "vitest": "^4.1.11"
112
112
  }
113
113
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.23",
3
+ "version": "0.26.26",
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",