grepmax 0.17.0 → 0.17.2

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.
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.dead = void 0;
46
+ const commander_1 = require("commander");
47
+ const graph_builder_1 = require("../lib/graph/graph-builder");
48
+ const vector_db_1 = require("../lib/store/vector-db");
49
+ const exit_1 = require("../lib/utils/exit");
50
+ const filter_builder_1 = require("../lib/utils/filter-builder");
51
+ const project_registry_1 = require("../lib/utils/project-registry");
52
+ const project_root_1 = require("../lib/utils/project-root");
53
+ const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
54
+ const style = {
55
+ bold: (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s),
56
+ dim: (s) => (useColors ? `\x1b[2m${s}\x1b[22m` : s),
57
+ red: (s) => (useColors ? `\x1b[31m${s}\x1b[39m` : s),
58
+ yellow: (s) => (useColors ? `\x1b[33m${s}\x1b[39m` : s),
59
+ green: (s) => (useColors ? `\x1b[32m${s}\x1b[39m` : s),
60
+ };
61
+ const TOP_CALLERS = 3;
62
+ function statusLabel(status) {
63
+ switch (status) {
64
+ case "DEAD":
65
+ return "DEAD";
66
+ case "PUBLIC_EXPORT":
67
+ return "PUBLIC EXPORT";
68
+ case "LIVE":
69
+ return "LIVE";
70
+ }
71
+ }
72
+ function formatHuman(r, projectRoot) {
73
+ const rel = (p) => p.startsWith(projectRoot) ? p.slice(projectRoot.length + 1) : p;
74
+ const defLoc = `${rel(r.defPath)}:${r.defLine + 1}`;
75
+ if (r.status === "DEAD") {
76
+ return `${style.red(style.bold("DEAD"))} ${defLoc} defines ${style.bold(r.symbol)}`;
77
+ }
78
+ if (r.status === "PUBLIC_EXPORT") {
79
+ return `${style.yellow(style.bold("PUBLIC EXPORT"))} ${defLoc} defines ${style.bold(r.symbol)} ${style.dim("— no internal callers found; check external usage")}`;
80
+ }
81
+ const header = `${style.green(style.bold("LIVE"))} ${defLoc} defines ${style.bold(r.symbol)} ${style.dim(`— ${r.callerCount} inbound caller${r.callerCount === 1 ? "" : "s"} (top ${Math.min(TOP_CALLERS, r.topCallers.length)}):`)}`;
82
+ const lines = [header];
83
+ for (const c of r.topCallers) {
84
+ lines.push(` ${rel(c.file)}:${c.line + 1}`);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+ function formatAgent(r, projectRoot) {
89
+ const rel = (p) => p.startsWith(projectRoot) ? p.slice(projectRoot.length + 1) : p;
90
+ const defLoc = `${rel(r.defPath)}:${r.defLine + 1}`;
91
+ const callerLocs = r.topCallers
92
+ .map((c) => `${rel(c.file)}:${c.line + 1}`)
93
+ .join(",");
94
+ return [
95
+ statusLabel(r.status),
96
+ defLoc,
97
+ String(r.callerCount),
98
+ callerLocs,
99
+ ].join("\t");
100
+ }
101
+ exports.dead = new commander_1.Command("dead")
102
+ .description("Report whether a symbol has zero inbound callers in the indexed call graph. " +
103
+ "The call graph reflects what tree-sitter chunked — dynamic dispatch, " +
104
+ "reflection, eval, and string-built call sites won't show up, so a 'DEAD' " +
105
+ "result is a hypothesis, not a proof. Exported public-API symbols " +
106
+ "legitimately have no in-project callers (reported as PUBLIC EXPORT).")
107
+ .argument("<symbol>", "The symbol to check")
108
+ .option("--root <dir>", "Project root directory")
109
+ .option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
110
+ .option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
111
+ .option("--agent", "Compact TSV output for AI agents", false)
112
+ .action((symbol, opts) => __awaiter(void 0, void 0, void 0, function* () {
113
+ var _a;
114
+ const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
115
+ if (root === null)
116
+ return;
117
+ let vectorDb = null;
118
+ try {
119
+ const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
120
+ const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
121
+ vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
122
+ const { resolveScope, buildScopeWhere } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/scope-filter")));
123
+ const scope = resolveScope({
124
+ projectRoot,
125
+ in: opts.in,
126
+ exclude: opts.exclude,
127
+ });
128
+ // Resolve the defining chunk to get path, line, and is_exported.
129
+ const table = yield vectorDb.ensureTable();
130
+ const defRows = yield table
131
+ .query()
132
+ .select(["path", "start_line", "is_exported"])
133
+ .where(buildScopeWhere(scope, `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}')`))
134
+ .limit(1)
135
+ .toArray();
136
+ if (defRows.length === 0) {
137
+ console.log(opts.agent ? "(not found)" : `Symbol not found: ${symbol}`);
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ const defRow = defRows[0];
142
+ const defPath = String(defRow.path || "");
143
+ const defLine = Number(defRow.start_line || 0);
144
+ const isExported = Boolean(defRow.is_exported);
145
+ const builder = new graph_builder_1.GraphBuilder(vectorDb, scope.pathPrefix, scope.excludePrefixes);
146
+ const callers = yield builder.getCallers(symbol);
147
+ const status = callers.length === 0
148
+ ? isExported
149
+ ? "PUBLIC_EXPORT"
150
+ : "DEAD"
151
+ : "LIVE";
152
+ const topCallers = callers
153
+ .slice(0, TOP_CALLERS)
154
+ .map((c) => ({ file: c.file, line: c.line }));
155
+ const result = {
156
+ status,
157
+ symbol,
158
+ defPath,
159
+ defLine,
160
+ callerCount: callers.length,
161
+ topCallers,
162
+ };
163
+ console.log(opts.agent
164
+ ? formatAgent(result, projectRoot)
165
+ : formatHuman(result, projectRoot));
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : "Unknown error";
169
+ console.error("Dead check failed:", message);
170
+ process.exitCode = 1;
171
+ }
172
+ finally {
173
+ if (vectorDb) {
174
+ try {
175
+ yield vectorDb.close();
176
+ }
177
+ catch (err) {
178
+ console.error("Failed to close VectorDB:", err);
179
+ }
180
+ }
181
+ yield (0, exit_1.gracefulExit)();
182
+ }
183
+ }));
@@ -161,6 +161,18 @@ const TOOLS = [
161
161
  required: ["symbol"],
162
162
  },
163
163
  },
164
+ {
165
+ name: "dead",
166
+ description: "Report whether a symbol has zero inbound callers in the indexed call graph. Returns DEAD, PUBLIC EXPORT (exported with no internal callers), or LIVE with caller count. Hypothesis, not proof: dynamic dispatch, reflection, and string-built call sites won't show up.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ symbol: { type: "string", description: "Symbol name to check" },
171
+ root: { type: "string", description: "Project root (absolute path)" },
172
+ },
173
+ required: ["symbol"],
174
+ },
175
+ },
164
176
  {
165
177
  name: "list_symbols",
166
178
  description: "List indexed symbols with role and export status.",
@@ -521,7 +533,7 @@ exports.mcp = new commander_1.Command("mcp")
521
533
  }
522
534
  }
523
535
  }
524
- const result = yield searcher.search(query, limit, { rerank: true }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
536
+ const result = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
525
537
  if (!result.data || result.data.length === 0) {
526
538
  return ok("No matches found. Try broadening your query, using fewer keywords, or check `gmax status` to verify the project is indexed.");
527
539
  }
@@ -1113,6 +1125,57 @@ exports.mcp = new commander_1.Command("mcp")
1113
1125
  }
1114
1126
  });
1115
1127
  }
1128
+ function handleDead(args) {
1129
+ return __awaiter(this, void 0, void 0, function* () {
1130
+ ensureWatcher();
1131
+ const symbol = String(args.symbol || "");
1132
+ if (!symbol)
1133
+ return err("Missing required parameter: symbol");
1134
+ try {
1135
+ const root = typeof args.root === "string" && args.root
1136
+ ? args.root
1137
+ : projectRoot;
1138
+ const db = getVectorDb();
1139
+ const table = yield db.ensureTable();
1140
+ const prefix = root.endsWith("/") ? root : `${root}/`;
1141
+ const defRows = yield table
1142
+ .query()
1143
+ .select(["path", "start_line", "is_exported"])
1144
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
1145
+ .limit(1)
1146
+ .toArray();
1147
+ if (defRows.length === 0) {
1148
+ return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
1149
+ }
1150
+ const defRow = defRows[0];
1151
+ const defPath = String(defRow.path || "");
1152
+ const defLine = Number(defRow.start_line || 0);
1153
+ const isExported = Boolean(defRow.is_exported);
1154
+ const builder = new graph_builder_1.GraphBuilder(db, root);
1155
+ const callers = yield builder.getCallers(symbol);
1156
+ const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
1157
+ const defLoc = `${rel(defPath)}:${defLine + 1}`;
1158
+ if (callers.length === 0) {
1159
+ if (isExported) {
1160
+ return ok(`PUBLIC EXPORT ${defLoc} defines ${symbol} — no internal callers found; check external usage`);
1161
+ }
1162
+ return ok(`DEAD ${defLoc} defines ${symbol}`);
1163
+ }
1164
+ const top = callers.slice(0, 3);
1165
+ const lines = [
1166
+ `LIVE ${defLoc} defines ${symbol} — ${callers.length} inbound caller${callers.length === 1 ? "" : "s"} (top ${top.length}):`,
1167
+ ];
1168
+ for (const c of top) {
1169
+ lines.push(` ${rel(c.file)}:${c.line + 1}`);
1170
+ }
1171
+ return ok(lines.join("\n"));
1172
+ }
1173
+ catch (e) {
1174
+ const msg = e instanceof Error ? e.message : String(e);
1175
+ return err(`Dead check failed: ${msg}`);
1176
+ }
1177
+ });
1178
+ }
1116
1179
  function handleListSymbols(args) {
1117
1180
  return __awaiter(this, void 0, void 0, function* () {
1118
1181
  ensureWatcher();
@@ -1578,7 +1641,7 @@ exports.mcp = new commander_1.Command("mcp")
1578
1641
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
1579
1642
  if (query) {
1580
1643
  const searcher = getSearcher();
1581
- const response = yield searcher.search(query, limit, { rerank: true }, {}, projectRoot);
1644
+ const response = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
1582
1645
  const changedSet = new Set(changedFiles);
1583
1646
  let filtered = response.data.filter((r) => changedSet.has(String(r.path || "")));
1584
1647
  if (role)
@@ -1753,7 +1816,7 @@ exports.mcp = new commander_1.Command("mcp")
1753
1816
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 25);
1754
1817
  try {
1755
1818
  const searcher = getSearcher();
1756
- const response = yield searcher.search(topic, limit, { rerank: true }, {}, projectRoot);
1819
+ const response = yield searcher.search(topic, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
1757
1820
  if (response.data.length === 0)
1758
1821
  return ok(`No results found for "${topic}".`);
1759
1822
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
@@ -1831,6 +1894,9 @@ exports.mcp = new commander_1.Command("mcp")
1831
1894
  case "peek_symbol":
1832
1895
  result = yield handlePeekSymbol(toolArgs);
1833
1896
  break;
1897
+ case "dead":
1898
+ result = yield handleDead(toolArgs);
1899
+ break;
1834
1900
  case "list_symbols":
1835
1901
  result = yield handleListSymbols(toolArgs);
1836
1902
  break;
@@ -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
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ const path = __importStar(require("node:path"));
40
40
  const commander_1 = require("commander");
41
41
  const add_1 = require("./commands/add");
42
42
  const context_1 = require("./commands/context");
43
+ const dead_1 = require("./commands/dead");
43
44
  const diff_1 = require("./commands/diff");
44
45
  const claude_code_1 = require("./commands/claude-code");
45
46
  const codex_1 = require("./commands/codex");
@@ -114,6 +115,7 @@ commander_1.program.addCommand(symbols_1.symbols);
114
115
  commander_1.program.addCommand(trace_1.trace);
115
116
  commander_1.program.addCommand(extract_1.extract);
116
117
  commander_1.program.addCommand(peek_1.peek);
118
+ commander_1.program.addCommand(dead_1.dead);
117
119
  commander_1.program.addCommand(project_1.project);
118
120
  commander_1.program.addCommand(related_1.related);
119
121
  commander_1.program.addCommand(log_1.log);
@@ -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.2",
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.2",
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",