grepmax 0.16.10 → 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.
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/search.js +2 -2
- package/dist/eval.js +50 -21
- package/dist/lib/daemon/daemon.js +56 -1
- package/dist/lib/daemon/ipc-handler.js +22 -1
- package/dist/lib/search/searcher.js +5 -1
- package/dist/lib/utils/daemon-client.js +5 -0
- package/dist/lib/workers/orchestrator.js +39 -22
- package/dist/lib/workers/pool.js +54 -5
- package/package.json +3 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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;
|
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/eval.js
CHANGED
|
@@ -572,11 +572,19 @@ function run() {
|
|
|
572
572
|
process.exit(1);
|
|
573
573
|
}
|
|
574
574
|
const results = [];
|
|
575
|
-
|
|
575
|
+
const jsonModeEarly = process.env.GMAX_EVAL_JSON === "1" || process.argv.includes("--json");
|
|
576
|
+
// In JSON mode, route all human-readable preamble to stderr so stdout
|
|
577
|
+
// stays a single parseable JSON object.
|
|
578
|
+
const log = jsonModeEarly ? console.error : console.log;
|
|
579
|
+
log("Starting evaluation...\n");
|
|
576
580
|
const startTime = performance.now();
|
|
581
|
+
// Rerank is OFF by default — measures pre-rerank quality so ranking-only
|
|
582
|
+
// changes show up clearly. Set GMAX_EVAL_RERANK=1 to measure the full
|
|
583
|
+
// production pipeline (slower, but more representative).
|
|
584
|
+
const rerank = process.env.GMAX_EVAL_RERANK === "1";
|
|
577
585
|
for (const c of exports.cases) {
|
|
578
586
|
const queryStart = performance.now();
|
|
579
|
-
const res = yield searcher.search(c.query, topK, { rerank
|
|
587
|
+
const res = yield searcher.search(c.query, topK, { rerank });
|
|
580
588
|
const queryEnd = performance.now();
|
|
581
589
|
const timeMs = queryEnd - queryStart;
|
|
582
590
|
results.push(evaluateCase(res, c, timeMs));
|
|
@@ -585,25 +593,46 @@ function run() {
|
|
|
585
593
|
const mrr = results.reduce((sum, r) => sum + r.rr, 0) / results.length;
|
|
586
594
|
const recallAt10 = results.reduce((sum, r) => sum + r.recall, 0) / results.length;
|
|
587
595
|
const avgTime = results.reduce((sum, r) => sum + r.timeMs, 0) / results.length;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
596
|
+
const summary = {
|
|
597
|
+
cases: results.length,
|
|
598
|
+
found: results.filter((r) => r.found).length,
|
|
599
|
+
hitsAt1: results.filter((r) => r.rr === 1).length,
|
|
600
|
+
mrrAt10: Number(mrr.toFixed(4)),
|
|
601
|
+
recallAt10: Number(recallAt10.toFixed(4)),
|
|
602
|
+
avgTimeMs: Math.round(avgTime),
|
|
603
|
+
totalTimeMs: Math.round(totalTime),
|
|
604
|
+
storePath: paths.lancedbDir,
|
|
605
|
+
rerank,
|
|
606
|
+
};
|
|
607
|
+
// JSON mode (GMAX_EVAL_JSON=1 or --json arg) emits a single stable object
|
|
608
|
+
// on stdout for tooling. Human output goes to stderr so both can be
|
|
609
|
+
// captured separately. Public-compatible shape — adding fixtures later
|
|
610
|
+
// doesn't change the schema.
|
|
611
|
+
const jsonMode = process.env.GMAX_EVAL_JSON === "1" || process.argv.includes("--json");
|
|
612
|
+
if (jsonMode) {
|
|
613
|
+
process.stdout.write(`${JSON.stringify({ summary, results }, null, 2)}\n`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
console.log("=".repeat(80));
|
|
617
|
+
console.log(`Eval results for store at: ${paths.lancedbDir}`);
|
|
618
|
+
console.log("=".repeat(80));
|
|
619
|
+
results.forEach((r) => {
|
|
620
|
+
const status = r.found ? `rank ${(1 / r.rr).toFixed(0)}` : "❌ missed";
|
|
621
|
+
const emoji = r.found ? (r.rr === 1 ? "🎯" : "✓") : "❌";
|
|
622
|
+
console.log(`${emoji} ${r.query}`);
|
|
623
|
+
console.log(` => ${status} (target: ${r.path}) [${r.timeMs.toFixed(0)}ms]`);
|
|
624
|
+
if (r.note) {
|
|
625
|
+
console.log(` // ${r.note}`);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
console.log("=".repeat(80));
|
|
629
|
+
console.log(`MRR: ${mrr.toFixed(3)}`);
|
|
630
|
+
console.log(`Recall@10: ${recallAt10.toFixed(3)}`);
|
|
631
|
+
console.log(`Avg query time: ${avgTime.toFixed(0)}ms`);
|
|
632
|
+
console.log(`Total time: ${totalTime.toFixed(0)}ms`);
|
|
633
|
+
console.log(`Found: ${summary.found}/${results.length}`);
|
|
634
|
+
console.log("=".repeat(80));
|
|
635
|
+
}
|
|
607
636
|
yield (0, exit_1.gracefulExit)(0);
|
|
608
637
|
});
|
|
609
638
|
}
|
|
@@ -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") {
|
|
@@ -910,6 +920,7 @@ class Daemon {
|
|
|
910
920
|
conn.on("close", () => ac.abort());
|
|
911
921
|
this.shutdownAbortControllers.add(ac);
|
|
912
922
|
this.vectorDb.pauseMaintenanceLoop();
|
|
923
|
+
const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
|
|
913
924
|
let lastProgressTime = 0;
|
|
914
925
|
try {
|
|
915
926
|
const result = yield (0, syncer_1.initialSync)({
|
|
@@ -934,6 +945,7 @@ class Daemon {
|
|
|
934
945
|
if (!this.shuttingDown) {
|
|
935
946
|
yield this.watchProject(root);
|
|
936
947
|
}
|
|
948
|
+
stopHeartbeat();
|
|
937
949
|
(0, ipc_handler_1.writeDone)(conn, {
|
|
938
950
|
ok: true,
|
|
939
951
|
processed: result.processed,
|
|
@@ -945,9 +957,11 @@ class Daemon {
|
|
|
945
957
|
catch (err) {
|
|
946
958
|
const msg = err instanceof Error ? err.message : String(err);
|
|
947
959
|
console.error(`[daemon] addProject failed for ${path.basename(root)}:`, msg);
|
|
960
|
+
stopHeartbeat();
|
|
948
961
|
(0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
|
|
949
962
|
}
|
|
950
963
|
finally {
|
|
964
|
+
stopHeartbeat();
|
|
951
965
|
this.shutdownAbortControllers.delete(ac);
|
|
952
966
|
(_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
|
|
953
967
|
}
|
|
@@ -977,6 +991,7 @@ class Daemon {
|
|
|
977
991
|
conn.on("close", () => ac.abort());
|
|
978
992
|
this.shutdownAbortControllers.add(ac);
|
|
979
993
|
this.vectorDb.pauseMaintenanceLoop();
|
|
994
|
+
const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
|
|
980
995
|
let lastProgressTime = 0;
|
|
981
996
|
try {
|
|
982
997
|
const result = yield (0, syncer_1.initialSync)({
|
|
@@ -1000,6 +1015,7 @@ class Daemon {
|
|
|
1000
1015
|
});
|
|
1001
1016
|
},
|
|
1002
1017
|
});
|
|
1018
|
+
stopHeartbeat();
|
|
1003
1019
|
(0, ipc_handler_1.writeDone)(conn, {
|
|
1004
1020
|
ok: true,
|
|
1005
1021
|
processed: result.processed,
|
|
@@ -1011,9 +1027,11 @@ class Daemon {
|
|
|
1011
1027
|
catch (err) {
|
|
1012
1028
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1013
1029
|
console.error(`[daemon] indexProject failed for ${path.basename(root)}:`, msg);
|
|
1030
|
+
stopHeartbeat();
|
|
1014
1031
|
(0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
|
|
1015
1032
|
}
|
|
1016
1033
|
finally {
|
|
1034
|
+
stopHeartbeat();
|
|
1017
1035
|
this.shutdownAbortControllers.delete(ac);
|
|
1018
1036
|
(_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
|
|
1019
1037
|
// Re-enable watcher (skip if shutting down)
|
|
@@ -1036,6 +1054,7 @@ class Daemon {
|
|
|
1036
1054
|
(0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
|
|
1037
1055
|
return;
|
|
1038
1056
|
}
|
|
1057
|
+
const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
|
|
1039
1058
|
try {
|
|
1040
1059
|
yield this.unwatchProject(root);
|
|
1041
1060
|
const rootPrefix = root.endsWith("/") ? root : `${root}/`;
|
|
@@ -1044,13 +1063,18 @@ class Daemon {
|
|
|
1044
1063
|
for (const key of keys) {
|
|
1045
1064
|
this.metaCache.delete(key);
|
|
1046
1065
|
}
|
|
1066
|
+
stopHeartbeat();
|
|
1047
1067
|
(0, ipc_handler_1.writeDone)(conn, { ok: true });
|
|
1048
1068
|
}
|
|
1049
1069
|
catch (err) {
|
|
1050
1070
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1051
1071
|
console.error(`[daemon] removeProject failed for ${path.basename(root)}:`, msg);
|
|
1072
|
+
stopHeartbeat();
|
|
1052
1073
|
(0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
|
|
1053
1074
|
}
|
|
1075
|
+
finally {
|
|
1076
|
+
stopHeartbeat();
|
|
1077
|
+
}
|
|
1054
1078
|
}));
|
|
1055
1079
|
});
|
|
1056
1080
|
}
|
|
@@ -1063,6 +1087,7 @@ class Daemon {
|
|
|
1063
1087
|
return;
|
|
1064
1088
|
}
|
|
1065
1089
|
const rootPrefix = (_a = opts.pathPrefix) !== null && _a !== void 0 ? _a : (root.endsWith("/") ? root : `${root}/`);
|
|
1090
|
+
const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
|
|
1066
1091
|
let lastProgressTime = 0;
|
|
1067
1092
|
try {
|
|
1068
1093
|
const result = yield (0, syncer_1.generateSummaries)(this.vectorDb, rootPrefix, (done, total) => {
|
|
@@ -1073,6 +1098,7 @@ class Daemon {
|
|
|
1073
1098
|
lastProgressTime = now;
|
|
1074
1099
|
(0, ipc_handler_1.writeProgress)(conn, { summarized: done, total });
|
|
1075
1100
|
}, opts.limit);
|
|
1101
|
+
stopHeartbeat();
|
|
1076
1102
|
(0, ipc_handler_1.writeDone)(conn, {
|
|
1077
1103
|
ok: true,
|
|
1078
1104
|
summarized: result.summarized,
|
|
@@ -1082,8 +1108,12 @@ class Daemon {
|
|
|
1082
1108
|
catch (err) {
|
|
1083
1109
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1084
1110
|
console.error(`[daemon] summarizeProject failed for ${path.basename(root)}:`, msg);
|
|
1111
|
+
stopHeartbeat();
|
|
1085
1112
|
(0, ipc_handler_1.writeDone)(conn, { ok: false, error: msg });
|
|
1086
1113
|
}
|
|
1114
|
+
finally {
|
|
1115
|
+
stopHeartbeat();
|
|
1116
|
+
}
|
|
1087
1117
|
}));
|
|
1088
1118
|
});
|
|
1089
1119
|
}
|
|
@@ -1165,6 +1195,31 @@ class Daemon {
|
|
|
1165
1195
|
return null;
|
|
1166
1196
|
}
|
|
1167
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
|
+
}
|
|
1168
1223
|
ensureMlxServer(mlxModel) {
|
|
1169
1224
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1170
1225
|
if (yield this.isMlxServerUp()) {
|
|
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.writeProgress = writeProgress;
|
|
46
46
|
exports.writeDone = writeDone;
|
|
47
|
+
exports.startHeartbeat = startHeartbeat;
|
|
47
48
|
exports.handleCommand = handleCommand;
|
|
48
49
|
const fs = __importStar(require("node:fs"));
|
|
49
50
|
const path = __importStar(require("node:path"));
|
|
@@ -73,6 +74,26 @@ function writeDone(conn, data) {
|
|
|
73
74
|
conn.write(`${JSON.stringify(Object.assign({ type: "done" }, data))}\n`);
|
|
74
75
|
conn.end();
|
|
75
76
|
}
|
|
77
|
+
const HEARTBEAT_INTERVAL_MS = 5000;
|
|
78
|
+
/**
|
|
79
|
+
* Emit periodic heartbeat lines so the client's watchdog timer resets even
|
|
80
|
+
* when no progress is reported (e.g., during a long DB flush or compaction).
|
|
81
|
+
* The client treats heartbeat lines as proof-of-life but not as progress.
|
|
82
|
+
*
|
|
83
|
+
* Returns a stop function; caller MUST call it before writeDone to avoid a
|
|
84
|
+
* stray heartbeat racing the done line.
|
|
85
|
+
*/
|
|
86
|
+
function startHeartbeat(conn, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
87
|
+
const timer = setInterval(() => {
|
|
88
|
+
if (!conn.writable) {
|
|
89
|
+
clearInterval(timer);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
conn.write(`${JSON.stringify({ type: "heartbeat", ts: Date.now() })}\n`);
|
|
93
|
+
}, intervalMs);
|
|
94
|
+
conn.once("close", () => clearInterval(timer));
|
|
95
|
+
return () => clearInterval(timer);
|
|
96
|
+
}
|
|
76
97
|
/**
|
|
77
98
|
* Handle a single IPC command.
|
|
78
99
|
*
|
|
@@ -148,7 +169,7 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
148
169
|
limit: limitRaw,
|
|
149
170
|
filters,
|
|
150
171
|
pathPrefix: typeof cmd.pathPrefix === "string" ? cmd.pathPrefix : undefined,
|
|
151
|
-
rerank: cmd.rerank
|
|
172
|
+
rerank: cmd.rerank === true,
|
|
152
173
|
explain: cmd.explain === true,
|
|
153
174
|
includeSkeletons: cmd.includeSkeletons === true,
|
|
154
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)();
|
|
@@ -219,6 +219,11 @@ function sendStreamingCommand(cmd, onProgress, opts) {
|
|
|
219
219
|
resetTimer();
|
|
220
220
|
onProgress(msg);
|
|
221
221
|
}
|
|
222
|
+
else if (msg.type === "heartbeat") {
|
|
223
|
+
// Proof-of-life from a daemon doing slow non-emitting work
|
|
224
|
+
// (DB flush, compaction). Reset the watchdog; do not surface.
|
|
225
|
+
resetTimer();
|
|
226
|
+
}
|
|
222
227
|
}
|
|
223
228
|
catch (_a) {
|
|
224
229
|
console.warn("[daemon-client] Malformed response line:", line.slice(0, 200));
|
|
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.WorkerOrchestrator = void 0;
|
|
46
|
+
exports.coerceColbertBytes = coerceColbertBytes;
|
|
46
47
|
const fs = __importStar(require("node:fs"));
|
|
47
48
|
const path = __importStar(require("node:path"));
|
|
48
49
|
const transformers_1 = require("@huggingface/transformers");
|
|
@@ -58,6 +59,43 @@ const granite_1 = require("./embeddings/granite");
|
|
|
58
59
|
const mlx_client_1 = require("./embeddings/mlx-client");
|
|
59
60
|
const logger_1 = require("../utils/logger");
|
|
60
61
|
let mlxFallbackWarned = false;
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a colbert payload received over IPC into an Int8Array.
|
|
64
|
+
*
|
|
65
|
+
* Node `child_process.send` serializes payloads as JSON, which doesn't
|
|
66
|
+
* preserve TypedArrays. An Int8Array sent over IPC arrives as a plain
|
|
67
|
+
* object with numeric keys (`{0: byte, 1: byte, ...}`). Without explicitly
|
|
68
|
+
* handling that shape, the rerank pipeline silently no-ops (empty matrix
|
|
69
|
+
* → maxSim returns 0 → final ranking falls back to fusion-only). Caught
|
|
70
|
+
* 2026-05-25 when the eval harness showed rerank-on producing identical
|
|
71
|
+
* scores to rerank-off across 97 cases.
|
|
72
|
+
*/
|
|
73
|
+
function coerceColbertBytes(col) {
|
|
74
|
+
if (col instanceof Int8Array)
|
|
75
|
+
return col;
|
|
76
|
+
if (Buffer.isBuffer(col)) {
|
|
77
|
+
return new Int8Array(col.buffer, col.byteOffset, col.byteLength);
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(col))
|
|
80
|
+
return new Int8Array(col);
|
|
81
|
+
if (col && typeof col === "object") {
|
|
82
|
+
// {type:"Buffer", data:[...]} — Node Buffer.toJSON output
|
|
83
|
+
const asUnknown = col;
|
|
84
|
+
if (asUnknown.type === "Buffer" && Array.isArray(asUnknown.data)) {
|
|
85
|
+
return new Int8Array(asUnknown.data);
|
|
86
|
+
}
|
|
87
|
+
// {0: byte, 1: byte, ...} — Int8Array after IPC JSON serialization
|
|
88
|
+
const keys = Object.keys(col);
|
|
89
|
+
if (keys.length > 0 && keys.every((k) => /^\d+$/.test(k))) {
|
|
90
|
+
const arr = new Int8Array(keys.length);
|
|
91
|
+
for (const k of keys) {
|
|
92
|
+
arr[Number(k)] = col[k];
|
|
93
|
+
}
|
|
94
|
+
return arr;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return new Int8Array(0);
|
|
98
|
+
}
|
|
61
99
|
const CACHE_DIR = config_1.PATHS.models;
|
|
62
100
|
const LOG_MODELS = process.env.GMAX_DEBUG_MODELS === "1" ||
|
|
63
101
|
process.env.GMAX_DEBUG_MODELS === "true";
|
|
@@ -330,28 +368,7 @@ class WorkerOrchestrator {
|
|
|
330
368
|
yield this.ensureReady();
|
|
331
369
|
const queryMatrix = input.query.map((row) => row instanceof Float32Array ? row : new Float32Array(row));
|
|
332
370
|
return input.docs.map((doc) => {
|
|
333
|
-
const
|
|
334
|
-
let colbert;
|
|
335
|
-
if (col instanceof Int8Array) {
|
|
336
|
-
colbert = col;
|
|
337
|
-
}
|
|
338
|
-
else if (Buffer.isBuffer(col)) {
|
|
339
|
-
colbert = new Int8Array(col.buffer, col.byteOffset, col.byteLength);
|
|
340
|
-
}
|
|
341
|
-
else if (col &&
|
|
342
|
-
typeof col === "object" &&
|
|
343
|
-
"type" in col &&
|
|
344
|
-
col.type === "Buffer" &&
|
|
345
|
-
Array.isArray(col.data)) {
|
|
346
|
-
// IPC serialization fallback (still copies, but unavoidable without SharedArrayBuffer)
|
|
347
|
-
colbert = new Int8Array(col.data);
|
|
348
|
-
}
|
|
349
|
-
else if (Array.isArray(col)) {
|
|
350
|
-
colbert = new Int8Array(col);
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
colbert = new Int8Array(0);
|
|
354
|
-
}
|
|
371
|
+
const colbert = coerceColbertBytes(doc.colbert);
|
|
355
372
|
const seqLen = Math.floor(colbert.length / input.colbertDim);
|
|
356
373
|
const docMatrix = [];
|
|
357
374
|
for (let i = 0; i < seqLen; i++) {
|
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;
|
|
@@ -90,6 +97,10 @@ class ProcessWorker {
|
|
|
90
97
|
this.busy = false;
|
|
91
98
|
this.pendingTaskId = null;
|
|
92
99
|
this.lastBusyTime = Date.now();
|
|
100
|
+
// Set when the pool has cleaned up after this worker (via exit or error
|
|
101
|
+
// event). Guards against handleWorkerExit running twice when both events
|
|
102
|
+
// fire for the same crash.
|
|
103
|
+
this.cleanedUp = false;
|
|
93
104
|
const memArgs = maxMemoryMb
|
|
94
105
|
? [`--max-old-space-size=${maxMemoryMb}`]
|
|
95
106
|
: [];
|
|
@@ -166,18 +177,26 @@ class WorkerPool {
|
|
|
166
177
|
worker.lastBusyTime = Date.now();
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
|
-
handleWorkerExit(worker, code, signal) {
|
|
180
|
+
handleWorkerExit(worker, code, signal, reason = "exit", err) {
|
|
170
181
|
var _a, _b, _c, _d;
|
|
182
|
+
// Crash paths can fire both 'error' and 'exit'. Either is sufficient
|
|
183
|
+
// to clean up; running this twice would double-respawn.
|
|
184
|
+
if (worker.cleanedUp)
|
|
185
|
+
return;
|
|
186
|
+
worker.cleanedUp = true;
|
|
171
187
|
worker.busy = false;
|
|
172
188
|
const failedTasks = Array.from(this.tasks.values()).filter((t) => t.worker === worker);
|
|
173
189
|
for (const task of failedTasks) {
|
|
174
190
|
this.clearTaskTimeout(task);
|
|
175
191
|
const filePath = (_d = (_b = (_a = task.payload) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : (_c = task.payload) === null || _c === void 0 ? void 0 : _c.absolutePath) !== null && _d !== void 0 ? _d : "unknown";
|
|
176
|
-
(0, logger_1.debug)("pool",
|
|
177
|
-
|
|
192
|
+
(0, logger_1.debug)("pool", `${reason} killed task=${task.id} method=${task.method} file=${filePath}`);
|
|
193
|
+
const exitDetail = err
|
|
194
|
+
? `: ${err.message}`
|
|
195
|
+
: `${code ? ` (code ${code})` : ""}${signal ? ` signal ${signal}` : ""}`;
|
|
196
|
+
task.reject(new Error(`Worker ${reason === "error" ? "errored" : "exited unexpectedly"}${exitDetail}`));
|
|
178
197
|
this.completeTask(task, null);
|
|
179
198
|
}
|
|
180
|
-
(0, logger_1.log)("pool", `Worker PID:${worker.child.pid}
|
|
199
|
+
(0, logger_1.log)("pool", `Worker PID:${worker.child.pid} ${reason} (code:${code} signal:${signal}${err ? ` err:${err.message}` : ""} pending=${failedTasks.length})`);
|
|
181
200
|
this.workers = this.workers.filter((w) => w !== worker);
|
|
182
201
|
if (!this.destroyed) {
|
|
183
202
|
// Only respawn if we have no workers left or there are pending tasks
|
|
@@ -243,9 +262,14 @@ class WorkerPool {
|
|
|
243
262
|
this.consecutiveRespawns = 0;
|
|
244
263
|
this.dispatch();
|
|
245
264
|
};
|
|
246
|
-
const onExit = (code, signal) => this.handleWorkerExit(worker, code, signal);
|
|
265
|
+
const onExit = (code, signal) => this.handleWorkerExit(worker, code, signal, "exit");
|
|
266
|
+
// 'error' fires when spawn fails, IPC send fails async, or the child
|
|
267
|
+
// can't be killed. Without this handler the worker stays in
|
|
268
|
+
// this.workers as a zombie that the next dispatch tries to send to.
|
|
269
|
+
const onError = (err) => this.handleWorkerExit(worker, null, null, "error", err);
|
|
247
270
|
worker.child.on("message", onMessage);
|
|
248
271
|
worker.child.on("exit", onExit);
|
|
272
|
+
worker.child.on("error", onError);
|
|
249
273
|
this.workers.push(worker);
|
|
250
274
|
}
|
|
251
275
|
enqueue(method, payload, signal) {
|
|
@@ -326,8 +350,10 @@ class WorkerPool {
|
|
|
326
350
|
(0, logger_1.log)("pool", `timeout task=${task.id} method=${task.method} file=${filePath} after ${TASK_TIMEOUT_MS}ms — killing worker PID:${worker.child.pid}`);
|
|
327
351
|
this.completeTask(task, null);
|
|
328
352
|
task.reject(new Error(`Worker task ${task.method} timed out after ${TASK_TIMEOUT_MS}ms on ${filePath}`));
|
|
353
|
+
worker.cleanedUp = true;
|
|
329
354
|
worker.child.removeAllListeners("message");
|
|
330
355
|
worker.child.removeAllListeners("exit");
|
|
356
|
+
worker.child.removeAllListeners("error");
|
|
331
357
|
try {
|
|
332
358
|
worker.child.kill("SIGKILL");
|
|
333
359
|
}
|
|
@@ -421,13 +447,34 @@ class WorkerPool {
|
|
|
421
447
|
.slice(0, reapCount)
|
|
422
448
|
.forEach((w) => {
|
|
423
449
|
(0, logger_1.log)("pool", `reap idle worker PID:${w.child.pid} (idle ${Math.round((now - w.lastBusyTime) / 1000)}s, ${this.workers.length - 1} remaining)`);
|
|
450
|
+
w.cleanedUp = true;
|
|
424
451
|
w.child.removeAllListeners("message");
|
|
425
452
|
w.child.removeAllListeners("exit");
|
|
453
|
+
w.child.removeAllListeners("error");
|
|
454
|
+
const pid = w.child.pid;
|
|
426
455
|
try {
|
|
427
456
|
w.child.kill("SIGTERM");
|
|
428
457
|
}
|
|
429
458
|
catch (_a) { }
|
|
430
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
|
+
}
|
|
431
478
|
});
|
|
432
479
|
}
|
|
433
480
|
destroy() {
|
|
@@ -449,8 +496,10 @@ class WorkerPool {
|
|
|
449
496
|
this.taskQueue = [];
|
|
450
497
|
this.priorityQueue = [];
|
|
451
498
|
const killPromises = this.workers.map((w) => new Promise((resolve) => {
|
|
499
|
+
w.cleanedUp = true;
|
|
452
500
|
w.child.removeAllListeners("message");
|
|
453
501
|
w.child.removeAllListeners("exit");
|
|
502
|
+
w.child.removeAllListeners("error");
|
|
454
503
|
w.child.once("exit", () => resolve());
|
|
455
504
|
w.child.kill("SIGTERM");
|
|
456
505
|
const force = setTimeout(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "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": {
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"benchmark:index": "./run-benchmark.sh $HOME/gmax-benchmarks --index",
|
|
27
27
|
"benchmark:agent": "npx tsx src/bench/benchmark-agent.ts",
|
|
28
28
|
"benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts",
|
|
29
|
+
"bench:recall": "npx tsx src/eval.ts",
|
|
30
|
+
"bench:recall:json": "GMAX_EVAL_JSON=1 npx tsx src/eval.ts",
|
|
29
31
|
"format": "biome check --write .",
|
|
30
32
|
"format:check": "biome check .",
|
|
31
33
|
"lint": "biome lint .",
|