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.
- package/dist/commands/dead.js +183 -0
- package/dist/commands/mcp.js +69 -3
- package/dist/commands/search.js +2 -2
- package/dist/index.js +2 -0
- package/dist/lib/daemon/daemon.js +36 -1
- package/dist/lib/daemon/ipc-handler.js +1 -1
- package/dist/lib/search/searcher.js +5 -1
- package/dist/lib/workers/pool.js +26 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -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
|
+
}));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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;
|
package/dist/commands/search.js
CHANGED
|
@@ -571,7 +571,7 @@ Examples:
|
|
|
571
571
|
? searchFilters
|
|
572
572
|
: undefined,
|
|
573
573
|
pathPrefix: pathFilter,
|
|
574
|
-
rerank:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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)();
|
package/dist/lib/workers/pool.js
CHANGED
|
@@ -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