grepmax 0.23.0 → 0.25.0
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +25 -6
- package/dist/commands/add.js +38 -31
- package/dist/commands/config.js +16 -15
- package/dist/commands/context.js +38 -13
- package/dist/commands/doctor.js +76 -83
- package/dist/commands/extract.js +12 -1
- package/dist/commands/impact.js +33 -1
- package/dist/commands/index.js +23 -24
- package/dist/commands/list.js +23 -14
- package/dist/commands/mcp.js +494 -162
- package/dist/commands/peek.js +16 -5
- package/dist/commands/repair.js +54 -120
- package/dist/commands/search-output.js +30 -9
- package/dist/commands/search-run.js +75 -47
- package/dist/commands/search-skeletons.js +28 -18
- package/dist/commands/search.js +45 -49
- package/dist/commands/serve.js +415 -373
- package/dist/commands/setup.js +2 -2
- package/dist/commands/similar.js +5 -5
- package/dist/commands/skeleton.js +67 -41
- package/dist/commands/status.js +5 -2
- package/dist/commands/summarize.js +6 -0
- package/dist/commands/surprises.js +150 -0
- package/dist/commands/watch.js +102 -38
- package/dist/config.js +3 -0
- package/dist/eval-surprising-connections.js +191 -0
- package/dist/index.js +2 -0
- package/dist/lib/analysis/surprising-connections.js +600 -0
- package/dist/lib/daemon/daemon.js +1101 -379
- package/dist/lib/daemon/ipc-handler.js +122 -13
- package/dist/lib/daemon/mlx-server-manager.js +268 -125
- package/dist/lib/daemon/process-manager.js +7 -4
- package/dist/lib/daemon/search-handler.js +23 -9
- package/dist/lib/daemon/watcher-manager.js +353 -110
- package/dist/lib/graph/impact-rollup.js +202 -0
- package/dist/lib/graph/impact.js +15 -1
- package/dist/lib/help/agent-cheatsheet.js +1 -0
- package/dist/lib/index/batch-processor.js +231 -67
- package/dist/lib/index/embedding-generation.js +109 -0
- package/dist/lib/index/embedding-status.js +23 -0
- package/dist/lib/index/file-policy.js +273 -0
- package/dist/lib/index/ignore-patterns.js +4 -0
- package/dist/lib/index/index-config.js +18 -4
- package/dist/lib/index/syncer.js +256 -79
- package/dist/lib/index/walker.js +66 -86
- package/dist/lib/index/watcher-batch.js +9 -0
- package/dist/lib/index/watcher.js +129 -3
- package/dist/lib/llm/server.js +6 -0
- package/dist/lib/search/searcher.js +30 -19
- package/dist/lib/skeleton/skeletonizer.js +118 -0
- package/dist/lib/skeleton/summary-formatter.js +8 -1
- package/dist/lib/store/store-lease.js +473 -0
- package/dist/lib/store/vector-db.js +302 -57
- package/dist/lib/utils/blocked-roots.js +63 -0
- package/dist/lib/utils/cross-project.js +7 -3
- package/dist/lib/utils/daemon-client.js +24 -1
- package/dist/lib/utils/daemon-launcher.js +38 -13
- package/dist/lib/utils/doctor-status.js +76 -0
- package/dist/lib/utils/file-utils.js +74 -4
- package/dist/lib/utils/keyed-mutex.js +101 -0
- package/dist/lib/utils/logger.js +57 -1
- package/dist/lib/utils/mlx-hf-cache.js +114 -0
- package/dist/lib/utils/operation-coordinator.js +146 -0
- package/dist/lib/utils/path-containment.js +106 -0
- package/dist/lib/utils/process.js +44 -0
- package/dist/lib/utils/project-registry.js +351 -3
- package/dist/lib/utils/scope-filter.js +3 -9
- package/dist/lib/utils/stale-hint.js +12 -19
- package/dist/lib/utils/watcher-launcher.js +5 -1
- package/dist/lib/utils/watcher-store.js +2 -2
- package/dist/lib/workers/colbert-math.js +15 -12
- package/dist/lib/workers/embeddings/colbert.js +3 -2
- package/dist/lib/workers/embeddings/granite.js +4 -3
- package/dist/lib/workers/embeddings/mlx-client.js +59 -16
- package/dist/lib/workers/orchestrator.js +39 -8
- package/dist/lib/workers/pool.js +150 -83
- package/dist/lib/workers/process-child.js +11 -2
- package/dist/lib/workers/serialized-handler.js +10 -0
- package/dist/lib/workers/worker.js +13 -4
- package/mlx-embed-server/server.py +21 -3
- package/mlx-embed-server/summarizer.py +8 -0
- package/package.json +6 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/start.js +3 -170
|
@@ -47,6 +47,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
47
47
|
});
|
|
48
48
|
};
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.validateMlxEmbeddingResponse = validateMlxEmbeddingResponse;
|
|
50
51
|
exports.isMlxUp = isMlxUp;
|
|
51
52
|
exports.mlxEmbed = mlxEmbed;
|
|
52
53
|
const http = __importStar(require("node:http"));
|
|
@@ -55,11 +56,31 @@ const MLX_PORT = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
|
55
56
|
const MLX_HOST = "127.0.0.1";
|
|
56
57
|
const MLX_TIMEOUT_MS = 10000;
|
|
57
58
|
const EMBED_MODE = process.env.GMAX_EMBED_MODE || "auto";
|
|
59
|
+
const FLOAT32_MAX = 3.4028234663852886e38;
|
|
58
60
|
let mlxAvailable = null;
|
|
59
61
|
let lastCheck = 0;
|
|
60
62
|
const CHECK_INTERVAL_MS = 30000;
|
|
61
63
|
let lastMlxWarning = 0;
|
|
62
64
|
const MLX_WARNING_INTERVAL_MS = 60000;
|
|
65
|
+
let checkedModel;
|
|
66
|
+
function validateMlxEmbeddingResponse(data, textCount, expectedModel, expectedDim) {
|
|
67
|
+
if (!data || typeof data !== "object")
|
|
68
|
+
return false;
|
|
69
|
+
const response = data;
|
|
70
|
+
return (typeof response.model === "string" &&
|
|
71
|
+
(!expectedModel || response.model === expectedModel) &&
|
|
72
|
+
typeof response.dim === "number" &&
|
|
73
|
+
Number.isInteger(response.dim) &&
|
|
74
|
+
(expectedDim === undefined || response.dim === expectedDim) &&
|
|
75
|
+
Array.isArray(response.vectors) &&
|
|
76
|
+
response.vectors.length === textCount &&
|
|
77
|
+
response.vectors.every((vector) => Array.isArray(vector) &&
|
|
78
|
+
vector.length === response.dim &&
|
|
79
|
+
(expectedDim === undefined || vector.length === expectedDim) &&
|
|
80
|
+
vector.every((element) => typeof element === "number" &&
|
|
81
|
+
Number.isFinite(element) &&
|
|
82
|
+
Math.abs(element) <= FLOAT32_MAX)));
|
|
83
|
+
}
|
|
63
84
|
function postJSON(reqPath, body) {
|
|
64
85
|
const start = performance.now();
|
|
65
86
|
return new Promise((resolve) => {
|
|
@@ -106,15 +127,24 @@ function postJSON(reqPath, body) {
|
|
|
106
127
|
/**
|
|
107
128
|
* Check if MLX server is reachable. Caches result for CHECK_INTERVAL_MS.
|
|
108
129
|
*/
|
|
109
|
-
function checkHealth() {
|
|
130
|
+
function checkHealth(expectedModel) {
|
|
110
131
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
132
|
const start = performance.now();
|
|
112
133
|
return new Promise((resolve) => {
|
|
113
134
|
const req = http.get({ hostname: MLX_HOST, port: MLX_PORT, path: "/health", timeout: 2000 }, (res) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
const chunks = [];
|
|
136
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
137
|
+
res.on("end", () => {
|
|
138
|
+
let model;
|
|
139
|
+
try {
|
|
140
|
+
model = JSON.parse(Buffer.concat(chunks).toString("utf8")).model;
|
|
141
|
+
}
|
|
142
|
+
catch (_a) { }
|
|
143
|
+
const ok = res.statusCode === 200 &&
|
|
144
|
+
(!expectedModel || model === expectedModel);
|
|
145
|
+
(0, logger_1.debug)("mlx", `health → ${ok ? "ok" : `status=${res.statusCode} model=${String(model)}`} ${(performance.now() - start).toFixed(0)}ms`);
|
|
146
|
+
resolve(ok);
|
|
147
|
+
});
|
|
118
148
|
});
|
|
119
149
|
req.on("error", (err) => {
|
|
120
150
|
(0, logger_1.debug)("mlx", `health → error: ${err.message} ${(performance.now() - start).toFixed(0)}ms`);
|
|
@@ -128,19 +158,21 @@ function checkHealth() {
|
|
|
128
158
|
});
|
|
129
159
|
});
|
|
130
160
|
}
|
|
131
|
-
function isMlxUp() {
|
|
161
|
+
function isMlxUp(expectedModel) {
|
|
132
162
|
return __awaiter(this, void 0, void 0, function* () {
|
|
133
163
|
const now = Date.now();
|
|
134
|
-
if (
|
|
164
|
+
if (checkedModel === expectedModel &&
|
|
165
|
+
mlxAvailable !== null &&
|
|
166
|
+
now - lastCheck < CHECK_INTERVAL_MS) {
|
|
135
167
|
(0, logger_1.debug)("mlx", `isMlxUp cached=${mlxAvailable} age=${now - lastCheck}ms`);
|
|
136
168
|
return mlxAvailable;
|
|
137
169
|
}
|
|
138
|
-
let result = yield checkHealth();
|
|
170
|
+
let result = yield checkHealth(expectedModel);
|
|
139
171
|
// On first check (cold start), retry once after 3s — server may still be loading
|
|
140
172
|
if (!result && mlxAvailable === null) {
|
|
141
173
|
console.log("[mlx] Embed server not ready, retrying in 3s...");
|
|
142
174
|
yield new Promise((r) => setTimeout(r, 3000));
|
|
143
|
-
result = yield checkHealth();
|
|
175
|
+
result = yield checkHealth(expectedModel);
|
|
144
176
|
if (result) {
|
|
145
177
|
console.log("[mlx] Embed server ready");
|
|
146
178
|
}
|
|
@@ -149,6 +181,7 @@ function isMlxUp() {
|
|
|
149
181
|
}
|
|
150
182
|
}
|
|
151
183
|
mlxAvailable = result;
|
|
184
|
+
checkedModel = expectedModel;
|
|
152
185
|
lastCheck = now;
|
|
153
186
|
return result;
|
|
154
187
|
});
|
|
@@ -157,16 +190,21 @@ function isMlxUp() {
|
|
|
157
190
|
* Get dense embeddings from MLX server.
|
|
158
191
|
* Returns Float32Array[] on success, null if server unavailable.
|
|
159
192
|
*/
|
|
160
|
-
function mlxEmbed(texts) {
|
|
193
|
+
function mlxEmbed(texts, options) {
|
|
161
194
|
return __awaiter(this, void 0, void 0, function* () {
|
|
162
|
-
|
|
195
|
+
var _a;
|
|
196
|
+
const mode = (_a = options === null || options === void 0 ? void 0 : options.mode) !== null && _a !== void 0 ? _a : EMBED_MODE;
|
|
197
|
+
if (mode === "cpu")
|
|
163
198
|
return null;
|
|
164
|
-
if (!(yield isMlxUp()))
|
|
199
|
+
if (!(yield isMlxUp(options === null || options === void 0 ? void 0 : options.expectedModel)))
|
|
165
200
|
return null;
|
|
166
201
|
(0, logger_1.debug)("mlx", `embed ${texts.length} texts`);
|
|
167
202
|
let postResult;
|
|
168
203
|
try {
|
|
169
|
-
postResult = yield postJSON("/embed", {
|
|
204
|
+
postResult = yield postJSON("/embed", {
|
|
205
|
+
texts,
|
|
206
|
+
expected_model: options === null || options === void 0 ? void 0 : options.expectedModel,
|
|
207
|
+
});
|
|
170
208
|
}
|
|
171
209
|
catch (error) {
|
|
172
210
|
mlxAvailable = false;
|
|
@@ -178,7 +216,8 @@ function mlxEmbed(texts) {
|
|
|
178
216
|
return null;
|
|
179
217
|
}
|
|
180
218
|
const { ok, data } = postResult;
|
|
181
|
-
|
|
219
|
+
const responseMatches = validateMlxEmbeddingResponse(data, texts.length, options === null || options === void 0 ? void 0 : options.expectedModel, options === null || options === void 0 ? void 0 : options.expectedDim);
|
|
220
|
+
if (!ok || !responseMatches) {
|
|
182
221
|
const wasPreviouslyAvailable = mlxAvailable !== false;
|
|
183
222
|
mlxAvailable = false;
|
|
184
223
|
const now = Date.now();
|
|
@@ -186,8 +225,12 @@ function mlxEmbed(texts) {
|
|
|
186
225
|
now - lastMlxWarning >= MLX_WARNING_INTERVAL_MS) {
|
|
187
226
|
console.error("[mlx] Embed server failed: bad response (ok=" +
|
|
188
227
|
ok +
|
|
189
|
-
",
|
|
190
|
-
|
|
228
|
+
", validResponse=" +
|
|
229
|
+
responseMatches +
|
|
230
|
+
", dim=" +
|
|
231
|
+
String(data === null || data === void 0 ? void 0 : data.dim) +
|
|
232
|
+
", model=" +
|
|
233
|
+
String(data === null || data === void 0 ? void 0 : data.model) +
|
|
191
234
|
")");
|
|
192
235
|
lastMlxWarning = now;
|
|
193
236
|
}
|
|
@@ -51,9 +51,11 @@ const ort = __importStar(require("onnxruntime-node"));
|
|
|
51
51
|
const uuid_1 = require("uuid");
|
|
52
52
|
const config_1 = require("../../config");
|
|
53
53
|
const chunker_1 = require("../index/chunker");
|
|
54
|
+
const embedding_generation_1 = require("../index/embedding-generation");
|
|
54
55
|
const skeleton_1 = require("../skeleton");
|
|
55
56
|
const file_utils_1 = require("../utils/file-utils");
|
|
56
57
|
const logger_1 = require("../utils/logger");
|
|
58
|
+
const path_containment_1 = require("../utils/path-containment");
|
|
57
59
|
const colbert_math_1 = require("./colbert-math");
|
|
58
60
|
const colbert_1 = require("./embeddings/colbert");
|
|
59
61
|
const granite_1 = require("./embeddings/granite");
|
|
@@ -115,13 +117,16 @@ if (fs.existsSync(LOCAL_MODELS)) {
|
|
|
115
117
|
log(`Worker: Using local models from ${LOCAL_MODELS}`);
|
|
116
118
|
}
|
|
117
119
|
class WorkerOrchestrator {
|
|
118
|
-
constructor(
|
|
119
|
-
this.
|
|
120
|
+
constructor(generation, embedMode) {
|
|
121
|
+
this.generation = generation;
|
|
122
|
+
this.embedMode = embedMode;
|
|
120
123
|
this.chunker = new chunker_1.TreeSitterChunker();
|
|
121
124
|
this.skeletonizer = new skeleton_1.Skeletonizer();
|
|
122
125
|
this.initPromise = null;
|
|
123
|
-
this.vectorDimensions = vectorDim
|
|
124
|
-
this.
|
|
126
|
+
this.vectorDimensions = generation.vectorDim;
|
|
127
|
+
this.allowOnnxFallback = (0, embedding_generation_1.isOnnxFallbackCompatible)(generation);
|
|
128
|
+
this.granite = new granite_1.GraniteModel(generation.vectorDim, generation.onnxModel);
|
|
129
|
+
this.colbert = new colbert_1.ColbertModel(generation.colbertModel);
|
|
125
130
|
}
|
|
126
131
|
ensureReady() {
|
|
127
132
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -159,17 +164,31 @@ class WorkerOrchestrator {
|
|
|
159
164
|
const BATCH_SIZE = Number.isFinite(envBatch) && envBatch > 0
|
|
160
165
|
? Math.max(4, Math.min(16, envBatch))
|
|
161
166
|
: 16;
|
|
167
|
+
let denseBackend = null;
|
|
162
168
|
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
163
169
|
if (i > 0)
|
|
164
170
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress();
|
|
165
171
|
const batchTexts = texts.slice(i, i + BATCH_SIZE);
|
|
166
172
|
const batchStart = performance.now();
|
|
167
173
|
// Try MLX GPU server first, fall back to ONNX CPU
|
|
168
|
-
const mlxResult =
|
|
174
|
+
const mlxResult = denseBackend === "onnx"
|
|
175
|
+
? null
|
|
176
|
+
: yield (0, mlx_client_1.mlxEmbed)(batchTexts, {
|
|
177
|
+
mode: this.embedMode,
|
|
178
|
+
expectedModel: this.generation.mlxModel,
|
|
179
|
+
expectedDim: this.generation.vectorDim,
|
|
180
|
+
});
|
|
181
|
+
if (!mlxResult && !this.allowOnnxFallback) {
|
|
182
|
+
throw new Error(`MLX embedding model ${this.generation.mlxModel} is unavailable; ONNX fallback is disabled for this custom embedding generation`);
|
|
183
|
+
}
|
|
184
|
+
if (!mlxResult && denseBackend === "mlx") {
|
|
185
|
+
throw new Error(`MLX embedding model ${this.generation.mlxModel} became unavailable during this embedding operation`);
|
|
186
|
+
}
|
|
169
187
|
if (!mlxResult && !mlxFallbackWarned) {
|
|
170
188
|
console.warn("[embed] MLX unavailable, falling back to CPU (ONNX)");
|
|
171
189
|
mlxFallbackWarned = true;
|
|
172
190
|
}
|
|
191
|
+
denseBackend = mlxResult ? "mlx" : "onnx";
|
|
173
192
|
const denseBatch = mlxResult !== null && mlxResult !== void 0 ? mlxResult : (yield this.granite.runBatch(batchTexts));
|
|
174
193
|
const colbertBatch = yield this.colbert.runBatch(batchTexts, denseBatch, this.vectorDimensions);
|
|
175
194
|
(0, logger_1.debug)("orch", `embed batch ${i / BATCH_SIZE + 1}/${Math.ceil(texts.length / BATCH_SIZE)} (${batchTexts.length} texts) mlx=${!!mlxResult} ${(performance.now() - batchStart).toFixed(0)}ms`);
|
|
@@ -243,8 +262,13 @@ class WorkerOrchestrator {
|
|
|
243
262
|
: input.absolutePath
|
|
244
263
|
? input.absolutePath
|
|
245
264
|
: path.join(PROJECT_ROOT, input.path);
|
|
265
|
+
(0, path_containment_1.resolveContainedPath)(input.projectRoot, absolutePath, {
|
|
266
|
+
verifyExistingTarget: true,
|
|
267
|
+
});
|
|
246
268
|
(0, logger_1.debug)("orch", `processFile start: ${input.path}`);
|
|
247
|
-
const { buffer, mtimeMs, size } = yield (0, file_utils_1.readFileSnapshot)(absolutePath
|
|
269
|
+
const { buffer, mtimeMs, size } = yield (0, file_utils_1.readFileSnapshot)(absolutePath, {
|
|
270
|
+
projectRoot: input.projectRoot,
|
|
271
|
+
});
|
|
248
272
|
const hash = (0, file_utils_1.computeContentHash)(buffer, absolutePath);
|
|
249
273
|
if (!(0, file_utils_1.isIndexableFile)(absolutePath, size)) {
|
|
250
274
|
(0, logger_1.debug)("orch", `skip ${input.path} (non-indexable, size=${size})`);
|
|
@@ -309,7 +333,14 @@ class WorkerOrchestrator {
|
|
|
309
333
|
var _a;
|
|
310
334
|
yield this.ensureReady();
|
|
311
335
|
// Try MLX GPU server first, fall back to ONNX CPU
|
|
312
|
-
const mlxResult = yield (0, mlx_client_1.mlxEmbed)([text]
|
|
336
|
+
const mlxResult = yield (0, mlx_client_1.mlxEmbed)([text], {
|
|
337
|
+
mode: this.embedMode,
|
|
338
|
+
expectedModel: this.generation.mlxModel,
|
|
339
|
+
expectedDim: this.generation.vectorDim,
|
|
340
|
+
});
|
|
341
|
+
if (!mlxResult && !this.allowOnnxFallback) {
|
|
342
|
+
throw new Error(`MLX embedding model ${this.generation.mlxModel} is unavailable; ONNX fallback is disabled for this custom embedding generation`);
|
|
343
|
+
}
|
|
313
344
|
const denseVector = (_a = mlxResult === null || mlxResult === void 0 ? void 0 : mlxResult[0]) !== null && _a !== void 0 ? _a : (yield this.granite.runBatch([text]))[0];
|
|
314
345
|
const encoded = yield this.colbert.encodeQuery(text);
|
|
315
346
|
const feeds = {
|
|
@@ -398,7 +429,7 @@ class WorkerOrchestrator {
|
|
|
398
429
|
const tokenIds = Array.isArray(doc.token_ids) && doc.token_ids.length === seqLen
|
|
399
430
|
? doc.token_ids
|
|
400
431
|
: undefined;
|
|
401
|
-
return (0, colbert_math_1.maxSim)(queryMatrix, docMatrix, tokenIds);
|
|
432
|
+
return (0, colbert_math_1.maxSim)(queryMatrix, docMatrix, tokenIds, this.generation.colbertModel);
|
|
402
433
|
});
|
|
403
434
|
});
|
|
404
435
|
}
|
package/dist/lib/workers/pool.js
CHANGED
|
@@ -55,6 +55,7 @@ const childProcess = __importStar(require("node:child_process"));
|
|
|
55
55
|
const fs = __importStar(require("node:fs"));
|
|
56
56
|
const path = __importStar(require("node:path"));
|
|
57
57
|
const config_1 = require("../../config");
|
|
58
|
+
const embedding_generation_1 = require("../index/embedding-generation");
|
|
58
59
|
const index_config_1 = require("../index/index-config");
|
|
59
60
|
const logger_1 = require("../utils/logger");
|
|
60
61
|
function reviveBufferLike(input) {
|
|
@@ -120,16 +121,19 @@ const REAP_FORCE_KILL_GRACE_MS = 5000;
|
|
|
120
121
|
* every worker user (index, add, search, daemon, mcp). Merged so process.env can
|
|
121
122
|
* override — see the spread order at fork().
|
|
122
123
|
*/
|
|
123
|
-
function embeddingEnv() {
|
|
124
|
-
const
|
|
125
|
-
const
|
|
124
|
+
function embeddingEnv(generation, embedMode) {
|
|
125
|
+
const config = generation && embedMode ? null : (0, index_config_1.readGlobalConfig)();
|
|
126
|
+
const resolved = generation !== null && generation !== void 0 ? generation : (0, embedding_generation_1.resolveEmbeddingGeneration)(config);
|
|
127
|
+
const mode = embedMode !== null && embedMode !== void 0 ? embedMode : config.embedMode;
|
|
126
128
|
return {
|
|
127
|
-
GMAX_VECTOR_DIM: String(
|
|
128
|
-
GMAX_EMBED_ONNX_MODEL:
|
|
129
|
+
GMAX_VECTOR_DIM: String(resolved.vectorDim),
|
|
130
|
+
GMAX_EMBED_ONNX_MODEL: resolved.onnxModel,
|
|
131
|
+
GMAX_EMBED_MODE: mode,
|
|
132
|
+
GMAX_EMBEDDING_GENERATION: JSON.stringify(resolved),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
class ProcessWorker {
|
|
132
|
-
constructor(modulePath, execArgv, maxMemoryMb) {
|
|
136
|
+
constructor(modulePath, execArgv, embeddingEnvironment, maxMemoryMb) {
|
|
133
137
|
this.modulePath = modulePath;
|
|
134
138
|
this.execArgv = execArgv;
|
|
135
139
|
this.busy = false;
|
|
@@ -140,16 +144,17 @@ class ProcessWorker {
|
|
|
140
144
|
this.busySince = null;
|
|
141
145
|
// Most recent RSS (bytes) the worker reported, for memory-based recycling.
|
|
142
146
|
this.lastRssBytes = 0;
|
|
147
|
+
// Consecutive post-task readings over WORKER_RSS_RECYCLE_MB. Recycling
|
|
148
|
+
// waits for BLOAT_STREAK_TO_RECYCLE of these; reset by any lean reading.
|
|
149
|
+
this.bloatStreak = 0;
|
|
143
150
|
// Set when the pool has cleaned up after this worker (via exit or error
|
|
144
151
|
// event). Guards against handleWorkerExit running twice when both events
|
|
145
152
|
// fire for the same crash.
|
|
146
153
|
this.cleanedUp = false;
|
|
147
154
|
const memArgs = maxMemoryMb ? [`--max-old-space-size=${maxMemoryMb}`] : [];
|
|
148
|
-
// embeddingEnv() first so a manually-exported GMAX_VECTOR_DIM /
|
|
149
|
-
// GMAX_EMBED_ONNX_MODEL in process.env still wins (spread last).
|
|
150
155
|
this.child = childProcess.fork(modulePath, {
|
|
151
156
|
execArgv: [...memArgs, ...execArgv],
|
|
152
|
-
env: Object.assign(Object.assign({},
|
|
157
|
+
env: Object.assign(Object.assign({}, process.env), embeddingEnvironment),
|
|
153
158
|
});
|
|
154
159
|
}
|
|
155
160
|
}
|
|
@@ -160,7 +165,10 @@ function resolveProcessWorker() {
|
|
|
160
165
|
return { filename: jsWorker, execArgv: [] };
|
|
161
166
|
}
|
|
162
167
|
if (fs.existsSync(tsWorker)) {
|
|
163
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
filename: tsWorker,
|
|
170
|
+
execArgv: ["--import", require.resolve("tsx")],
|
|
171
|
+
};
|
|
164
172
|
}
|
|
165
173
|
throw new Error("Process worker file not found");
|
|
166
174
|
}
|
|
@@ -171,17 +179,28 @@ const IDLE_WORKER_TIMEOUT_MS = 60000; // reap idle workers after 60s
|
|
|
171
179
|
// load models) on the rare search is preferable to keeping a second worker
|
|
172
180
|
// warm. The pool still scales up to maxWorkers on demand for indexing bursts.
|
|
173
181
|
const MIN_KEEP_WORKERS = 1;
|
|
174
|
-
// Recycle
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
182
|
+
// Recycle a worker whose RSS has grown past this. ONNX native memory lives
|
|
183
|
+
// outside V8, so --max-old-space-size can't bound it; replacing the worker is
|
|
184
|
+
// the only reclaim. The threshold must sit ABOVE the healthy working set or
|
|
185
|
+
// the pool executes warm workers for normal behavior: measured steady state
|
|
186
|
+
// is ~700-800 MB with MLX serving dense embeds and ~900-1050 MB in ONNX CPU
|
|
187
|
+
// fallback (granite session loaded in-process), flat across task count and
|
|
188
|
+
// file size. The old 800 default was calibrated for the pre-
|
|
189
|
+
// enableCpuMemArena:false era (arenas pinned ~2 GB) and sat inside the
|
|
190
|
+
// healthy range — every busy worker was recycled after nearly every task,
|
|
191
|
+
// paying a full model reload each time. 0 (or negative) disables the check.
|
|
178
192
|
const WORKER_RSS_RECYCLE_MB = (() => {
|
|
179
193
|
var _a;
|
|
180
194
|
const fromEnv = Number.parseInt((_a = process.env.GMAX_WORKER_RSS_RECYCLE_MB) !== null && _a !== void 0 ? _a : "", 10);
|
|
181
195
|
if (Number.isFinite(fromEnv))
|
|
182
196
|
return fromEnv;
|
|
183
|
-
return
|
|
197
|
+
return 1536;
|
|
184
198
|
})();
|
|
199
|
+
// Post-task recycling waits for this many consecutive over-threshold readings.
|
|
200
|
+
// V8 holds freed heap after a big file, so a single post-task reading can show
|
|
201
|
+
// a transient high-water mark that the next task's GC releases; the second
|
|
202
|
+
// reading confirms the memory is actually sticky.
|
|
203
|
+
const BLOAT_STREAK_TO_RECYCLE = 2;
|
|
185
204
|
// Methods that must skip the indexing backlog. encodeQuery is the search hot
|
|
186
205
|
// path: a single query is ~17ms but waits behind every queued processFile.
|
|
187
206
|
// rerank is similarly small and latency-sensitive.
|
|
@@ -190,7 +209,7 @@ const PRIORITY_METHODS = new Set([
|
|
|
190
209
|
"rerank",
|
|
191
210
|
]);
|
|
192
211
|
class WorkerPool {
|
|
193
|
-
constructor() {
|
|
212
|
+
constructor(generation, embedMode) {
|
|
194
213
|
this.workers = [];
|
|
195
214
|
// Two queues so searches don't wait behind a long indexing backlog. Priority
|
|
196
215
|
// tasks (encodeQuery/rerank) are dispatched first; processFile tasks queue
|
|
@@ -198,7 +217,6 @@ class WorkerPool {
|
|
|
198
217
|
this.priorityQueue = [];
|
|
199
218
|
this.taskQueue = [];
|
|
200
219
|
this.tasks = new Map();
|
|
201
|
-
this.abortedTasks = new Set();
|
|
202
220
|
this.nextId = 1;
|
|
203
221
|
this.destroyed = false;
|
|
204
222
|
this.destroyPromise = null;
|
|
@@ -209,6 +227,14 @@ class WorkerPool {
|
|
|
209
227
|
this.modulePath = resolved.filename;
|
|
210
228
|
this.execArgv = resolved.execArgv;
|
|
211
229
|
this.maxWorkers = Math.max(1, config_1.CONFIG.WORKER_THREADS);
|
|
230
|
+
const config = generation && embedMode ? null : (0, index_config_1.readGlobalConfig)();
|
|
231
|
+
this.generation = generation !== null && generation !== void 0 ? generation : (0, embedding_generation_1.resolveEmbeddingGeneration)(config);
|
|
232
|
+
this.embedMode = embedMode !== null && embedMode !== void 0 ? embedMode : config.embedMode;
|
|
233
|
+
const generatedEmbeddingEnv = embeddingEnv(this.generation, this.embedMode);
|
|
234
|
+
// Replacement workers must use the same embedding generation as the pool
|
|
235
|
+
// that created them, even if config changes while tasks are running. Other
|
|
236
|
+
// process environment remains live and is copied at each fork.
|
|
237
|
+
this.embeddingEnvironment = Object.freeze(generatedEmbeddingEnv);
|
|
212
238
|
// Lazy spawn: start with 1 worker, scale up on demand
|
|
213
239
|
this.spawnWorker();
|
|
214
240
|
// Periodically reap idle workers back to MIN_KEEP, and force-kill any
|
|
@@ -280,6 +306,10 @@ class WorkerPool {
|
|
|
280
306
|
}
|
|
281
307
|
completeTask(task, worker) {
|
|
282
308
|
this.clearTaskTimeout(task);
|
|
309
|
+
if (task.signal && task.abortListener) {
|
|
310
|
+
task.signal.removeEventListener("abort", task.abortListener);
|
|
311
|
+
task.abortListener = undefined;
|
|
312
|
+
}
|
|
283
313
|
this.tasks.delete(task.id);
|
|
284
314
|
this.removeFromQueue(task.id);
|
|
285
315
|
if (worker) {
|
|
@@ -320,23 +350,12 @@ class WorkerPool {
|
|
|
320
350
|
}
|
|
321
351
|
}
|
|
322
352
|
spawnWorker() {
|
|
323
|
-
const worker = new ProcessWorker(this.modulePath, this.execArgv, config_1.MAX_WORKER_MEMORY_MB);
|
|
353
|
+
const worker = new ProcessWorker(this.modulePath, this.execArgv, this.embeddingEnvironment, config_1.MAX_WORKER_MEMORY_MB);
|
|
324
354
|
(0, logger_1.log)("pool", `spawn PID:${worker.child.pid} (${this.workers.length + 1}/${Math.max(1, config_1.CONFIG.WORKER_THREADS)})`);
|
|
325
355
|
const onMessage = (msg) => {
|
|
326
356
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
327
357
|
if (typeof msg.rss === "number")
|
|
328
358
|
worker.lastRssBytes = msg.rss;
|
|
329
|
-
// Fast cleanup for tasks that were aborted while running
|
|
330
|
-
if (this.abortedTasks.has(msg.id)) {
|
|
331
|
-
this.abortedTasks.delete(msg.id);
|
|
332
|
-
const task = this.tasks.get(msg.id);
|
|
333
|
-
if (task) {
|
|
334
|
-
this.completeTask(task, worker);
|
|
335
|
-
this.recycleIfBloated(worker);
|
|
336
|
-
this.dispatch();
|
|
337
|
-
}
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
359
|
const task = this.tasks.get(msg.id);
|
|
341
360
|
if (!task)
|
|
342
361
|
return;
|
|
@@ -416,34 +435,26 @@ class WorkerPool {
|
|
|
416
435
|
payload,
|
|
417
436
|
resolve: safeResolve,
|
|
418
437
|
reject: safeReject,
|
|
438
|
+
signal,
|
|
419
439
|
};
|
|
420
440
|
if (signal) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const err = new Error("Aborted");
|
|
432
|
-
err.name = "AbortError";
|
|
433
|
-
safeReject(err);
|
|
434
|
-
}
|
|
435
|
-
// If task is already running (assigned to worker), we can't easily kill it without
|
|
436
|
-
// killing the worker. For now, we just let it finish but reject the promise early so
|
|
437
|
-
// the caller doesn't wait. The worker will eventually finish and we'll ignore the result.
|
|
438
|
-
else if (this.tasks.has(id)) {
|
|
439
|
-
// Task is running. Reject caller immediately.
|
|
440
|
-
const err = new Error("Aborted");
|
|
441
|
-
err.name = "AbortError";
|
|
442
|
-
safeReject(err);
|
|
443
|
-
// Track for fast cleanup when the worker eventually finishes.
|
|
444
|
-
this.abortedTasks.add(id);
|
|
441
|
+
task.abortListener = () => {
|
|
442
|
+
if (!this.tasks.has(id))
|
|
443
|
+
return;
|
|
444
|
+
const err = new Error("Aborted");
|
|
445
|
+
err.name = "AbortError";
|
|
446
|
+
safeReject(err);
|
|
447
|
+
if (!task.worker) {
|
|
448
|
+
this.completeTask(task, null);
|
|
449
|
+
this.dispatch();
|
|
450
|
+
return;
|
|
445
451
|
}
|
|
446
|
-
|
|
452
|
+
// Caller settlement and worker lifecycle settlement are separate.
|
|
453
|
+
// Keep the assignment and timers until the child sends a terminal
|
|
454
|
+
// result/error or exits.
|
|
455
|
+
task.callerAborted = true;
|
|
456
|
+
};
|
|
457
|
+
signal.addEventListener("abort", task.abortListener, { once: true });
|
|
447
458
|
}
|
|
448
459
|
this.tasks.set(id, task);
|
|
449
460
|
if (PRIORITY_METHODS.has(method)) {
|
|
@@ -590,14 +601,17 @@ class WorkerPool {
|
|
|
590
601
|
this.recycleWorker(w, "idle");
|
|
591
602
|
}
|
|
592
603
|
/**
|
|
593
|
-
* Recycle a worker whose RSS
|
|
594
|
-
*
|
|
595
|
-
* churn (a busy monorepo trickling one small file at a time) a
|
|
596
|
-
* dispatched again within the 60s idle window, so a
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
604
|
+
* Recycle a worker whose RSS stays over the threshold across consecutive
|
|
605
|
+
* task completions. The idle reaper alone can't catch this: under
|
|
606
|
+
* continuous churn (a busy monorepo trickling one small file at a time) a
|
|
607
|
+
* worker is dispatched again within the 60s idle window, so a genuinely
|
|
608
|
+
* pinned worker never looks "idle". Checking at task completion — when busy
|
|
609
|
+
* was just cleared and before the next dispatch — bounds RSS regardless of
|
|
610
|
+
* how steady the churn is. Requires BLOAT_STREAK_TO_RECYCLE consecutive
|
|
611
|
+
* over-threshold readings so a one-off high-water mark (V8 holding heap it
|
|
612
|
+
* frees on the next task's GC) doesn't execute a warm worker and force a
|
|
613
|
+
* model reload. No-op while busy, so an in-flight task is never
|
|
614
|
+
* interrupted.
|
|
601
615
|
*/
|
|
602
616
|
recycleIfBloated(worker) {
|
|
603
617
|
if (this.destroyed ||
|
|
@@ -607,7 +621,13 @@ class WorkerPool {
|
|
|
607
621
|
return;
|
|
608
622
|
}
|
|
609
623
|
if (worker.lastRssBytes > WORKER_RSS_RECYCLE_MB * 1024 * 1024) {
|
|
610
|
-
|
|
624
|
+
worker.bloatStreak++;
|
|
625
|
+
if (worker.bloatStreak >= BLOAT_STREAK_TO_RECYCLE) {
|
|
626
|
+
this.recycleWorker(worker, "post-task");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
worker.bloatStreak = 0;
|
|
611
631
|
}
|
|
612
632
|
}
|
|
613
633
|
/**
|
|
@@ -702,7 +722,7 @@ class WorkerPool {
|
|
|
702
722
|
});
|
|
703
723
|
}
|
|
704
724
|
destroy() {
|
|
705
|
-
return __awaiter(this,
|
|
725
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
706
726
|
if (this.destroyPromise)
|
|
707
727
|
return this.destroyPromise;
|
|
708
728
|
if (this.destroyed)
|
|
@@ -713,46 +733,86 @@ class WorkerPool {
|
|
|
713
733
|
this.idleReapInterval = null;
|
|
714
734
|
}
|
|
715
735
|
for (const task of this.tasks.values()) {
|
|
716
|
-
this.clearTaskTimeout(task);
|
|
717
736
|
task.reject(new Error("Worker pool destroyed"));
|
|
737
|
+
this.completeTask(task, null);
|
|
718
738
|
}
|
|
719
|
-
this.tasks.clear();
|
|
720
739
|
this.taskQueue = [];
|
|
721
740
|
this.priorityQueue = [];
|
|
722
|
-
const
|
|
741
|
+
const exitedWorkers = new Set();
|
|
742
|
+
const killPromises = this.workers.map((w) => new Promise((resolve, reject) => {
|
|
723
743
|
var _a, _b;
|
|
724
744
|
let settled = false;
|
|
725
745
|
let force;
|
|
726
|
-
let
|
|
727
|
-
const
|
|
746
|
+
let deadline;
|
|
747
|
+
const settle = (error) => {
|
|
728
748
|
if (settled)
|
|
729
749
|
return;
|
|
730
750
|
settled = true;
|
|
731
751
|
if (force)
|
|
732
752
|
clearTimeout(force);
|
|
733
|
-
if (
|
|
734
|
-
clearTimeout(
|
|
735
|
-
|
|
753
|
+
if (deadline)
|
|
754
|
+
clearTimeout(deadline);
|
|
755
|
+
if (error)
|
|
756
|
+
reject(error);
|
|
757
|
+
else
|
|
758
|
+
resolve();
|
|
759
|
+
};
|
|
760
|
+
const exited = () => {
|
|
761
|
+
exitedWorkers.add(w);
|
|
762
|
+
settle();
|
|
736
763
|
};
|
|
737
764
|
w.cleanedUp = true;
|
|
738
765
|
w.child.removeAllListeners("message");
|
|
739
766
|
w.child.removeAllListeners("exit");
|
|
740
767
|
w.child.removeAllListeners("error");
|
|
741
|
-
w.child.once("exit",
|
|
768
|
+
w.child.once("exit", exited);
|
|
742
769
|
force = setTimeout(() => {
|
|
743
770
|
try {
|
|
744
|
-
w.child.kill("SIGKILL")
|
|
771
|
+
if (!w.child.kill("SIGKILL") && options.requireExit) {
|
|
772
|
+
settle(new Error(`Failed to SIGKILL worker PID:${w.child.pid}`));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
if (options.requireExit) {
|
|
777
|
+
settle(new Error(`Failed to SIGKILL worker PID:${w.child.pid}: ${error instanceof Error ? error.message : String(error)}`));
|
|
778
|
+
}
|
|
745
779
|
}
|
|
746
|
-
catch (_a) { }
|
|
747
780
|
}, FORCE_KILL_GRACE_MS);
|
|
748
781
|
(_a = force.unref) === null || _a === void 0 ? void 0 : _a.call(force);
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
782
|
+
deadline = setTimeout(() => {
|
|
783
|
+
if (options.requireExit) {
|
|
784
|
+
settle(new Error(`Worker PID:${w.child.pid} did not exit within ${config_1.WORKER_TIMEOUT_MS}ms`));
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
settle();
|
|
788
|
+
}
|
|
789
|
+
}, config_1.WORKER_TIMEOUT_MS);
|
|
790
|
+
(_b = deadline.unref) === null || _b === void 0 ? void 0 : _b.call(deadline);
|
|
791
|
+
try {
|
|
792
|
+
if (!w.child.kill("SIGTERM") && options.requireExit) {
|
|
793
|
+
settle(new Error(`Failed to SIGTERM worker PID:${w.child.pid}`));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
if (options.requireExit) {
|
|
798
|
+
settle(new Error(`Failed to SIGTERM worker PID:${w.child.pid}: ${error instanceof Error ? error.message : String(error)}`));
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
settle();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
752
804
|
}));
|
|
753
|
-
this.destroyPromise = Promise.allSettled(killPromises).then(() => {
|
|
754
|
-
this.workers =
|
|
755
|
-
|
|
805
|
+
this.destroyPromise = Promise.allSettled(killPromises).then((results) => {
|
|
806
|
+
this.workers = options.requireExit
|
|
807
|
+
? this.workers.filter((worker) => !exitedWorkers.has(worker))
|
|
808
|
+
: [];
|
|
809
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
810
|
+
if (failures.length > 0) {
|
|
811
|
+
const details = failures
|
|
812
|
+
.map((failure) => failure instanceof Error ? failure.message : String(failure))
|
|
813
|
+
.join("; ");
|
|
814
|
+
throw new Error(`Strict worker pool shutdown failed: ${details}`);
|
|
815
|
+
}
|
|
756
816
|
});
|
|
757
817
|
yield this.destroyPromise;
|
|
758
818
|
});
|
|
@@ -761,9 +821,16 @@ class WorkerPool {
|
|
|
761
821
|
exports.WorkerPool = WorkerPool;
|
|
762
822
|
WorkerPool.MAX_RESPAWNS = 10;
|
|
763
823
|
let singleton = null;
|
|
764
|
-
function getWorkerPool() {
|
|
824
|
+
function getWorkerPool(generation, embedMode) {
|
|
765
825
|
if (!singleton) {
|
|
766
|
-
singleton = new WorkerPool();
|
|
826
|
+
singleton = new WorkerPool(generation, embedMode);
|
|
827
|
+
}
|
|
828
|
+
else if (generation &&
|
|
829
|
+
singleton.generation.fingerprint !== generation.fingerprint) {
|
|
830
|
+
throw new Error(`Worker pool embedding generation mismatch: active ${singleton.generation.fingerprint}, requested ${generation.fingerprint}`);
|
|
831
|
+
}
|
|
832
|
+
else if (embedMode && singleton.embedMode !== embedMode) {
|
|
833
|
+
throw new Error(`Worker pool embed mode mismatch: active ${singleton.embedMode}, requested ${embedMode}`);
|
|
767
834
|
}
|
|
768
835
|
return singleton;
|
|
769
836
|
}
|