grepmax 0.17.0 → 0.17.1

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.
@@ -521,7 +521,7 @@ exports.mcp = new commander_1.Command("mcp")
521
521
  }
522
522
  }
523
523
  }
524
- const result = yield searcher.search(query, limit, { rerank: true }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
524
+ const result = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
525
525
  if (!result.data || result.data.length === 0) {
526
526
  return ok("No matches found. Try broadening your query, using fewer keywords, or check `gmax status` to verify the project is indexed.");
527
527
  }
@@ -1578,7 +1578,7 @@ exports.mcp = new commander_1.Command("mcp")
1578
1578
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
1579
1579
  if (query) {
1580
1580
  const searcher = getSearcher();
1581
- const response = yield searcher.search(query, limit, { rerank: true }, {}, projectRoot);
1581
+ const response = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
1582
1582
  const changedSet = new Set(changedFiles);
1583
1583
  let filtered = response.data.filter((r) => changedSet.has(String(r.path || "")));
1584
1584
  if (role)
@@ -1753,7 +1753,7 @@ exports.mcp = new commander_1.Command("mcp")
1753
1753
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 25);
1754
1754
  try {
1755
1755
  const searcher = getSearcher();
1756
- const response = yield searcher.search(topic, limit, { rerank: true }, {}, projectRoot);
1756
+ const response = yield searcher.search(topic, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
1757
1757
  if (response.data.length === 0)
1758
1758
  return ok(`No results found for "${topic}".`);
1759
1759
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
@@ -571,7 +571,7 @@ Examples:
571
571
  ? searchFilters
572
572
  : undefined,
573
573
  pathPrefix: pathFilter,
574
- rerank: true,
574
+ rerank: process.env.GMAX_RERANK === "1",
575
575
  explain: options.explain,
576
576
  includeSkeletons: options.skeleton,
577
577
  includeGraph: options.symbol,
@@ -668,7 +668,7 @@ Examples:
668
668
  }
669
669
  }
670
670
  const searcher = new searcher_1.Searcher(vectorDb);
671
- searchResult = yield searcher.search(pattern, parseInt(options.m, 10), { rerank: true, explain: options.explain }, Object.keys(searchFilters).length > 0
671
+ searchResult = yield searcher.search(pattern, parseInt(options.m, 10), { rerank: process.env.GMAX_RERANK === "1", explain: options.explain }, Object.keys(searchFilters).length > 0
672
672
  ? searchFilters
673
673
  : undefined, pathFilter);
674
674
  } // end if (!searchResult) — in-process fallback
@@ -109,6 +109,8 @@ class Daemon {
109
109
  this.startTime = Date.now();
110
110
  this.heartbeatInterval = null;
111
111
  this.idleInterval = null;
112
+ this.heartbeatTick = 0;
113
+ this.mlxRecoveryInFlight = false;
112
114
  this.shuttingDown = false;
113
115
  this.pendingOps = new Set();
114
116
  this.watcherFailCount = new Map();
@@ -289,6 +291,14 @@ class Daemon {
289
291
  }
290
292
  catch (_a) { }
291
293
  (0, log_rotate_1.rotateLogFds)(path.join(config_1.PATHS.logsDir, "daemon.log"));
294
+ // Every 5 ticks (5 min), probe the MLX embed server and respawn if
295
+ // it's gone zombie (port held but /health unresponsive). Closes the
296
+ // 42h-degradation window where workers silently fell back to ONNX CPU
297
+ // after a frozen MLX process kept the port bound (v0.17.0 bug #1).
298
+ this.heartbeatTick++;
299
+ if (this.heartbeatTick % 5 === 0) {
300
+ void this.checkMlxHealth();
301
+ }
292
302
  }, HEARTBEAT_INTERVAL_MS);
293
303
  // 10. Idle timeout (skip when disabled via env)
294
304
  if (IDLE_TIMEOUT_MS > 0) {
@@ -796,7 +806,7 @@ class Daemon {
796
806
  this.lastActivity = Date.now();
797
807
  let result;
798
808
  try {
799
- result = yield searcher.search(payload.query, payload.limit, { rerank: payload.rerank !== false, explain: payload.explain === true }, payload.filters, payload.pathPrefix, undefined, signal);
809
+ result = yield searcher.search(payload.query, payload.limit, { rerank: payload.rerank === true, explain: payload.explain === true }, payload.filters, payload.pathPrefix, undefined, signal);
800
810
  }
801
811
  catch (err) {
802
812
  if ((err === null || err === void 0 ? void 0 : err.name) === "AbortError") {
@@ -1185,6 +1195,31 @@ class Daemon {
1185
1195
  return null;
1186
1196
  }
1187
1197
  }
1198
+ checkMlxHealth() {
1199
+ return __awaiter(this, void 0, void 0, function* () {
1200
+ if (this.shuttingDown || this.mlxRecoveryInFlight)
1201
+ return;
1202
+ if (yield this.isMlxServerUp())
1203
+ return;
1204
+ const port = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
1205
+ const stalePid = this.getPortPid(port);
1206
+ if (!stalePid)
1207
+ return; // No process — let the next user-facing path spawn it.
1208
+ this.mlxRecoveryInFlight = true;
1209
+ try {
1210
+ console.log(`[daemon] MLX zombie detected on port ${port} (PID ${stalePid}) — killing and respawning`);
1211
+ yield (0, process_1.killProcess)(stalePid);
1212
+ yield new Promise((r) => setTimeout(r, 500));
1213
+ yield this.ensureMlxServer();
1214
+ }
1215
+ catch (err) {
1216
+ console.error(`[daemon] MLX recovery failed: ${err instanceof Error ? err.message : String(err)}`);
1217
+ }
1218
+ finally {
1219
+ this.mlxRecoveryInFlight = false;
1220
+ }
1221
+ });
1222
+ }
1188
1223
  ensureMlxServer(mlxModel) {
1189
1224
  return __awaiter(this, void 0, void 0, function* () {
1190
1225
  if (yield this.isMlxServerUp()) {
@@ -169,7 +169,7 @@ function handleCommand(daemon, cmd, conn) {
169
169
  limit: limitRaw,
170
170
  filters,
171
171
  pathPrefix: typeof cmd.pathPrefix === "string" ? cmd.pathPrefix : undefined,
172
- rerank: cmd.rerank !== false,
172
+ rerank: cmd.rerank === true,
173
173
  explain: cmd.explain === true,
174
174
  includeSkeletons: cmd.includeSkeletons === true,
175
175
  skeletonLimit: skeletonLimitRaw,
@@ -340,7 +340,11 @@ class Searcher {
340
340
  return __awaiter(this, void 0, void 0, function* () {
341
341
  var _a, _b, _c, _d, _e, _f, _g, _h;
342
342
  const finalLimit = top_k !== null && top_k !== void 0 ? top_k : 10;
343
- const doRerank = (_a = _search_options === null || _search_options === void 0 ? void 0 : _search_options.rerank) !== null && _a !== void 0 ? _a : true;
343
+ // ColBERT rerank is opt-in as of v0.17.1. On the 97-case eval it
344
+ // regresses MRR@10 by ~3% and doubles query latency; sweep across
345
+ // FUSED_WEIGHT ∈ {0,0.1,0.5,1,2} showed rerank scores dominate
346
+ // fused scores ~30:1 so blend tuning can't recover the loss.
347
+ const doRerank = (_a = _search_options === null || _search_options === void 0 ? void 0 : _search_options.rerank) !== null && _a !== void 0 ? _a : false;
344
348
  const explain = (_b = _search_options === null || _search_options === void 0 ? void 0 : _search_options.explain) !== null && _b !== void 0 ? _b : false;
345
349
  const searchIntent = intent || (0, intent_1.detectIntent)(query);
346
350
  const pool = (0, pool_1.getWorkerPool)();
@@ -83,6 +83,13 @@ const TASK_TIMEOUT_MS = (() => {
83
83
  return 120000;
84
84
  })();
85
85
  const FORCE_KILL_GRACE_MS = 200;
86
+ // Longer grace for idle reaps: the worker isn't urgently in the way, and a
87
+ // graceful SIGTERM lets ONNX free ~1GB of model memory. But if SIGTERM is
88
+ // ignored (a worker burning 100% CPU inside a native ONNX matmul tight loop
89
+ // won't service signals — the 42h zombie we saw in v0.17.0 validation),
90
+ // escalate to SIGKILL. ~5s is well above ONNX teardown time but short
91
+ // enough that the reap loop self-heals within a minute.
92
+ const REAP_FORCE_KILL_GRACE_MS = 5000;
86
93
  class ProcessWorker {
87
94
  constructor(modulePath, execArgv, maxMemoryMb) {
88
95
  this.modulePath = modulePath;
@@ -444,11 +451,30 @@ class WorkerPool {
444
451
  w.child.removeAllListeners("message");
445
452
  w.child.removeAllListeners("exit");
446
453
  w.child.removeAllListeners("error");
454
+ const pid = w.child.pid;
447
455
  try {
448
456
  w.child.kill("SIGTERM");
449
457
  }
450
458
  catch (_a) { }
451
459
  this.workers = this.workers.filter((x) => x !== w);
460
+ // SIGTERM is ignored by a worker stuck inside a native ONNX matmul
461
+ // tight loop. Escalate to SIGKILL if the process is still alive after
462
+ // the grace period. Rare; warn-level so it's visible if it fires.
463
+ if (pid !== undefined) {
464
+ setTimeout(() => {
465
+ try {
466
+ process.kill(pid, 0);
467
+ (0, logger_1.log)("pool", `reap escalation: SIGTERM ignored by PID:${pid}, sending SIGKILL`);
468
+ try {
469
+ process.kill(pid, "SIGKILL");
470
+ }
471
+ catch (_a) { }
472
+ }
473
+ catch (_b) {
474
+ // ESRCH — process already gone, nothing to do.
475
+ }
476
+ }, REAP_FORCE_KILL_GRACE_MS);
477
+ }
452
478
  });
453
479
  }
454
480
  destroy() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
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.17.0",
3
+ "version": "0.17.1",
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",