grepmax 0.26.70 → 0.26.71
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/DOCUMENT-SEARCH.md +113 -0
- package/README.md +14 -0
- package/dist/bin.js +11 -2
- package/dist/lib/daemon/daemon.js +50 -0
- package/dist/lib/daemon/document-search-handler.js +194 -0
- package/dist/lib/daemon/ipc-handler.js +29 -2
- package/dist/lib/mcp/document-contract.js +281 -0
- package/dist/lib/mcp/document-search.js +274 -0
- package/dist/lib/store/vector-db.js +13 -0
- package/dist/lib/utils/resource-budget.js +29 -1
- package/dist/lib/workers/embeddings/granite.js +8 -3
- package/dist/lib/workers/embeddings/mlx-client.js +27 -0
- package/dist/lib/workers/orchestrator.js +47 -9
- package/dist/lib/workers/pool.js +85 -3
- package/dist/lib/workers/process-child.js +22 -5
- package/dist/lib/workers/worker.js +4 -1
- package/package.json +5 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -51,6 +51,8 @@ exports.validateMlxEmbeddingResponse = validateMlxEmbeddingResponse;
|
|
|
51
51
|
exports.requestMlxJSON = requestMlxJSON;
|
|
52
52
|
exports.isMlxUp = isMlxUp;
|
|
53
53
|
exports.mlxEmbed = mlxEmbed;
|
|
54
|
+
exports.isMlxExistingReady = isMlxExistingReady;
|
|
55
|
+
exports.mlxEmbedExisting = mlxEmbedExisting;
|
|
54
56
|
const http = __importStar(require("node:http"));
|
|
55
57
|
const logger_1 = require("../../utils/logger");
|
|
56
58
|
const MLX_PORT = parseInt(process.env.MLX_EMBED_PORT || "8100", 10);
|
|
@@ -267,3 +269,28 @@ function mlxEmbed(texts, options) {
|
|
|
267
269
|
return data.vectors.map((v) => new Float32Array(v));
|
|
268
270
|
});
|
|
269
271
|
}
|
|
272
|
+
/** Cached positive identity permits one bounded call to the existing backend.
|
|
273
|
+
* Every response verifies model/dimensions again; no polling or startup follows
|
|
274
|
+
* from an idle cache. A failed call revokes eligibility until external work
|
|
275
|
+
* confirms readiness again.
|
|
276
|
+
*/
|
|
277
|
+
function isMlxExistingReady(expectedModel) {
|
|
278
|
+
return mlxAvailable === true && checkedModel === expectedModel;
|
|
279
|
+
}
|
|
280
|
+
function mlxEmbedExisting(texts, options) {
|
|
281
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
282
|
+
if (!isMlxExistingReady(options.expectedModel))
|
|
283
|
+
throw new Error("embedding_unavailable");
|
|
284
|
+
const result = yield requestMlxJSON("/embed", {
|
|
285
|
+
texts,
|
|
286
|
+
expected_model: options.expectedModel,
|
|
287
|
+
});
|
|
288
|
+
if (!result.ok ||
|
|
289
|
+
!validateMlxEmbeddingResponse(result.data, texts.length, options.expectedModel, options.expectedDim)) {
|
|
290
|
+
mlxAvailable = false;
|
|
291
|
+
throw new Error("embedding_unavailable");
|
|
292
|
+
}
|
|
293
|
+
lastCheck = Date.now();
|
|
294
|
+
return result.data.vectors.map((v) => new Float32Array(v));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -54,9 +54,11 @@ const chunker_1 = require("../index/chunker");
|
|
|
54
54
|
const embedding_generation_1 = require("../index/embedding-generation");
|
|
55
55
|
const embedding_reuse_1 = require("../index/embedding-reuse");
|
|
56
56
|
const skeleton_1 = require("../skeleton");
|
|
57
|
+
const autostart_1 = require("../utils/autostart");
|
|
57
58
|
const file_utils_1 = require("../utils/file-utils");
|
|
58
59
|
const logger_1 = require("../utils/logger");
|
|
59
60
|
const path_containment_1 = require("../utils/path-containment");
|
|
61
|
+
const resource_budget_1 = require("../utils/resource-budget");
|
|
60
62
|
const colbert_math_1 = require("./colbert-math");
|
|
61
63
|
const embedding_error_1 = require("./embedding-error");
|
|
62
64
|
const colbert_1 = require("./embeddings/colbert");
|
|
@@ -323,20 +325,56 @@ class WorkerOrchestrator {
|
|
|
323
325
|
: { vectors, hash, mtimeMs, size };
|
|
324
326
|
});
|
|
325
327
|
}
|
|
326
|
-
|
|
327
|
-
return
|
|
328
|
+
isExistingQueryReady() {
|
|
329
|
+
return this.embedMode === "cpu"
|
|
330
|
+
? this.granite.isReady()
|
|
331
|
+
: (0, mlx_client_1.isMlxExistingReady)(this.generation.mlxModel);
|
|
332
|
+
}
|
|
333
|
+
encodeQuery(text_1) {
|
|
334
|
+
return __awaiter(this, arguments, void 0, function* (text, existingOnly = false, expectedGeneration) {
|
|
328
335
|
var _a;
|
|
329
|
-
|
|
336
|
+
if (existingOnly) {
|
|
337
|
+
if (expectedGeneration !== this.generation.fingerprint)
|
|
338
|
+
throw Object.assign(new Error("embedding_mismatch"), {
|
|
339
|
+
code: "embedding_mismatch",
|
|
340
|
+
});
|
|
341
|
+
try {
|
|
342
|
+
if ((0, autostart_1.daemonStartDeniedReason)())
|
|
343
|
+
throw new Error();
|
|
344
|
+
resource_budget_1.resourceBudget.checkExisting();
|
|
345
|
+
if ((0, autostart_1.daemonStartDeniedReason)())
|
|
346
|
+
throw new Error();
|
|
347
|
+
}
|
|
348
|
+
catch (_b) {
|
|
349
|
+
throw Object.assign(new Error("host_pressure"), {
|
|
350
|
+
code: "host_pressure",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (!this.isExistingQueryReady())
|
|
354
|
+
throw new Error("embedding_unavailable");
|
|
355
|
+
}
|
|
356
|
+
else
|
|
357
|
+
yield this.ensureReady();
|
|
330
358
|
// Try MLX GPU server first, fall back to ONNX CPU
|
|
331
|
-
const mlxResult =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
359
|
+
const mlxResult = existingOnly
|
|
360
|
+
? this.embedMode === "gpu"
|
|
361
|
+
? yield (0, mlx_client_1.mlxEmbedExisting)([text], {
|
|
362
|
+
mode: this.embedMode,
|
|
363
|
+
expectedModel: this.generation.mlxModel,
|
|
364
|
+
expectedDim: this.generation.vectorDim,
|
|
365
|
+
})
|
|
366
|
+
: null
|
|
367
|
+
: yield (0, mlx_client_1.mlxEmbed)([text], {
|
|
368
|
+
mode: this.embedMode,
|
|
369
|
+
expectedModel: this.generation.mlxModel,
|
|
370
|
+
expectedDim: this.generation.vectorDim,
|
|
371
|
+
});
|
|
336
372
|
if (!mlxResult && !this.allowOnnxFallback) {
|
|
337
373
|
throw new Error(`MLX embedding model ${this.generation.mlxModel} is unavailable; ONNX fallback is disabled for this custom embedding generation`);
|
|
338
374
|
}
|
|
339
|
-
const denseVector = (_a = mlxResult === null || mlxResult === void 0 ? void 0 : mlxResult[0]) !== null && _a !== void 0 ? _a : (yield this.granite.runBatch([text]))[0];
|
|
375
|
+
const denseVector = (_a = mlxResult === null || mlxResult === void 0 ? void 0 : mlxResult[0]) !== null && _a !== void 0 ? _a : (yield this.granite.runBatch([text], existingOnly))[0];
|
|
376
|
+
if (existingOnly)
|
|
377
|
+
return { dense: Array.from(denseVector), colbert: [], colbertDim: 0 };
|
|
340
378
|
const encoded = yield this.colbert.encodeQuery(text);
|
|
341
379
|
const feeds = {
|
|
342
380
|
input_ids: new ort.Tensor("int64", encoded.input_ids, [
|
package/dist/lib/workers/pool.js
CHANGED
|
@@ -122,6 +122,8 @@ class ProcessWorker {
|
|
|
122
122
|
this.modulePath = modulePath;
|
|
123
123
|
this.execArgv = execArgv;
|
|
124
124
|
this.busy = false;
|
|
125
|
+
this.queryReady = false;
|
|
126
|
+
this.suppressReplacement = false;
|
|
125
127
|
this.pendingTaskId = null;
|
|
126
128
|
this.lastBusyTime = Date.now();
|
|
127
129
|
// Wall-clock at which this worker became busy with its current task; null
|
|
@@ -383,7 +385,8 @@ class WorkerPool {
|
|
|
383
385
|
if (!this.destroyed) {
|
|
384
386
|
// Only respawn if we have no workers left or there are pending tasks
|
|
385
387
|
const hasPendingTasks = this.hasUnassignedTasks();
|
|
386
|
-
if (this.workers.length === 0 ||
|
|
388
|
+
if ((!worker.suppressReplacement && this.workers.length === 0) ||
|
|
389
|
+
hasPendingTasks) {
|
|
387
390
|
this.maybeRespawn(reason, hasPendingTasks);
|
|
388
391
|
}
|
|
389
392
|
this.dispatch();
|
|
@@ -467,12 +470,16 @@ class WorkerPool {
|
|
|
467
470
|
const onMessage = (msg) => {
|
|
468
471
|
var _a, _b, _c, _d;
|
|
469
472
|
var _e, _f, _g, _h;
|
|
473
|
+
if (typeof msg.queryReady === "boolean")
|
|
474
|
+
worker.queryReady = msg.queryReady;
|
|
470
475
|
if (typeof msg.rss === "number")
|
|
471
476
|
worker.lastRssBytes = msg.rss;
|
|
472
477
|
const task = this.tasks.get(msg.id);
|
|
473
478
|
if (!task)
|
|
474
479
|
return;
|
|
475
480
|
if ("heartbeat" in msg) {
|
|
481
|
+
if (task.existingOnly)
|
|
482
|
+
return;
|
|
476
483
|
// Reset only the no-progress timeout. The hard deadline is left
|
|
477
484
|
// untouched on purpose — heartbeats must not be able to extend a task
|
|
478
485
|
// past its absolute ceiling.
|
|
@@ -673,6 +680,7 @@ class WorkerPool {
|
|
|
673
680
|
: "aggregate resource admission unavailable");
|
|
674
681
|
return;
|
|
675
682
|
}
|
|
683
|
+
idle.suppressReplacement = false;
|
|
676
684
|
idle.busy = true;
|
|
677
685
|
idle.pendingTaskId = task.id;
|
|
678
686
|
idle.busySince = Date.now();
|
|
@@ -744,6 +752,78 @@ class WorkerPool {
|
|
|
744
752
|
processFile(input, signal) {
|
|
745
753
|
return this.enqueue("processFile", input, signal);
|
|
746
754
|
}
|
|
755
|
+
/** No queue, scaling or initialization for an existing-index document query. */
|
|
756
|
+
existingQueryState() {
|
|
757
|
+
if (this.destroyed || this.spawnDeniedReason || (0, autostart_1.daemonStartDeniedReason)())
|
|
758
|
+
return "host_pressure";
|
|
759
|
+
const warm = this.workers.filter((w) => w.queryReady && !w.cleanedUp && w.child.connected !== false);
|
|
760
|
+
return warm.some((w) => !w.busy)
|
|
761
|
+
? "ready"
|
|
762
|
+
: warm.length
|
|
763
|
+
? "busy"
|
|
764
|
+
: "embedding_unavailable";
|
|
765
|
+
}
|
|
766
|
+
encodeQueryExisting(text, signal) {
|
|
767
|
+
const state = this.existingQueryState();
|
|
768
|
+
if (state !== "ready")
|
|
769
|
+
return Promise.reject(Object.assign(new Error(state), { code: state }));
|
|
770
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
771
|
+
return Promise.reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
772
|
+
try {
|
|
773
|
+
resource_budget_1.resourceBudget.checkExisting();
|
|
774
|
+
}
|
|
775
|
+
catch (_a) {
|
|
776
|
+
return Promise.reject(Object.assign(new Error("host_pressure"), { code: "host_pressure" }));
|
|
777
|
+
}
|
|
778
|
+
// Recheck after admission: quarantine or worker readiness may have changed.
|
|
779
|
+
const after = this.existingQueryState();
|
|
780
|
+
if (after !== "ready")
|
|
781
|
+
return Promise.reject(Object.assign(new Error(after), { code: after }));
|
|
782
|
+
const worker = this.workers.find((w) => w.queryReady && !w.busy && !w.cleanedUp && w.child.connected !== false);
|
|
783
|
+
return new Promise((resolve, reject) => {
|
|
784
|
+
const id = this.nextId++;
|
|
785
|
+
const task = {
|
|
786
|
+
id,
|
|
787
|
+
method: "encodeQuery",
|
|
788
|
+
payload: {
|
|
789
|
+
text,
|
|
790
|
+
existingOnly: true,
|
|
791
|
+
generation: this.generation.fingerprint,
|
|
792
|
+
},
|
|
793
|
+
resolve,
|
|
794
|
+
reject,
|
|
795
|
+
queuedAt: Date.now(),
|
|
796
|
+
signal,
|
|
797
|
+
existingOnly: true,
|
|
798
|
+
worker,
|
|
799
|
+
};
|
|
800
|
+
const cancel = () => {
|
|
801
|
+
task.callerAborted = true;
|
|
802
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
803
|
+
};
|
|
804
|
+
task.abortListener = cancel;
|
|
805
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", cancel, { once: true });
|
|
806
|
+
// Caller deadline does not kill or replace this shared worker. Its bounded
|
|
807
|
+
// inference settles lifecycle independently, just like normal cancellation.
|
|
808
|
+
task.timeout = setTimeout(() => {
|
|
809
|
+
task.callerAborted = true;
|
|
810
|
+
reject(new Error("timeout"));
|
|
811
|
+
}, 10000);
|
|
812
|
+
worker.busy = true;
|
|
813
|
+
worker.suppressReplacement = true;
|
|
814
|
+
worker.busySince = Date.now();
|
|
815
|
+
worker.pendingTaskId = id;
|
|
816
|
+
this.tasks.set(id, task);
|
|
817
|
+
try {
|
|
818
|
+
worker.child.send({ id, method: task.method, payload: task.payload });
|
|
819
|
+
}
|
|
820
|
+
catch (_a) {
|
|
821
|
+
this.completeTask(task, worker);
|
|
822
|
+
worker.queryReady = false;
|
|
823
|
+
reject(new Error("embedding_unavailable"));
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
747
827
|
encodeQuery(text, signal) {
|
|
748
828
|
return this.enqueue("encodeQuery", { text }, signal);
|
|
749
829
|
}
|
|
@@ -826,7 +906,7 @@ class WorkerPool {
|
|
|
826
906
|
* floor with a fresh, lean worker. Shared by the idle and post-task RSS paths.
|
|
827
907
|
*/
|
|
828
908
|
recycleWorker(w, reason) {
|
|
829
|
-
if (w.cleanedUp)
|
|
909
|
+
if (w.cleanedUp || w.busy)
|
|
830
910
|
return;
|
|
831
911
|
(0, logger_1.log)("pool", `recycle bloated worker PID:${w.child.pid} (${reason}, rss ${Math.round(w.lastRssBytes / 1048576)}MB > ${WORKER_RSS_RECYCLE_MB}MB)`);
|
|
832
912
|
w.cleanedUp = true;
|
|
@@ -856,7 +936,9 @@ class WorkerPool {
|
|
|
856
936
|
}, REAP_FORCE_KILL_GRACE_MS);
|
|
857
937
|
}
|
|
858
938
|
// Replace anything we dropped below the floor with fresh, lean workers.
|
|
859
|
-
while (!
|
|
939
|
+
while (!w.suppressReplacement &&
|
|
940
|
+
!this.destroyed &&
|
|
941
|
+
this.workers.length < MIN_KEEP_WORKERS) {
|
|
860
942
|
if (!this.spawnWorker())
|
|
861
943
|
break;
|
|
862
944
|
}
|
|
@@ -59,11 +59,14 @@ const send = (msg) => {
|
|
|
59
59
|
if (node_process_1.default.send) {
|
|
60
60
|
// Attach current RSS so the pool can recycle workers whose native (ONNX)
|
|
61
61
|
// memory has ballooned — the V8 --max-old-space-size cap can't see it.
|
|
62
|
-
node_process_1.default.send(Object.assign(Object.assign({}, msg), { rss: node_process_1.default.memoryUsage().rss }));
|
|
62
|
+
node_process_1.default.send(Object.assign(Object.assign({}, msg), { rss: node_process_1.default.memoryUsage().rss, queryReady: (0, worker_1.isExistingQueryReady)() }));
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
|
+
let restrictedInFlight = false;
|
|
65
66
|
const handleMessage = (msg) => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
67
|
const { id, method, payload } = msg;
|
|
68
|
+
restrictedInFlight =
|
|
69
|
+
method === "encodeQuery" && payload.existingOnly === true;
|
|
67
70
|
const start = performance.now();
|
|
68
71
|
(0, logger_1.debug)("worker", `recv task=${id} method=${method}${method === "processFile" ? ` file=${payload.path}` : ""}`);
|
|
69
72
|
try {
|
|
@@ -91,23 +94,37 @@ const handleMessage = (msg) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
91
94
|
send({ id, error: `Unknown method: ${method}` });
|
|
92
95
|
}
|
|
93
96
|
catch (err) {
|
|
94
|
-
const
|
|
97
|
+
const rawCode = err === null || err === void 0 ? void 0 : err.code;
|
|
98
|
+
const safeCode = typeof rawCode === "string" &&
|
|
99
|
+
["host_pressure", "embedding_mismatch", "embedding_unavailable"].includes(rawCode)
|
|
100
|
+
? rawCode
|
|
101
|
+
: "embedding_unavailable";
|
|
102
|
+
const message = restrictedInFlight
|
|
103
|
+
? safeCode
|
|
104
|
+
: err instanceof Error
|
|
105
|
+
? err.message
|
|
106
|
+
: String(err);
|
|
95
107
|
(0, logger_1.debug)("worker", `fail task=${id} method=${method} ${(performance.now() - start).toFixed(0)}ms: ${message}`);
|
|
96
|
-
const code =
|
|
108
|
+
const code = restrictedInFlight
|
|
109
|
+
? safeCode
|
|
110
|
+
: err === null || err === void 0 ? void 0 : err.code;
|
|
97
111
|
send(Object.assign({ id, error: message }, (typeof code === "string" ? { code } : {})));
|
|
98
112
|
}
|
|
113
|
+
finally {
|
|
114
|
+
restrictedInFlight = false;
|
|
115
|
+
}
|
|
99
116
|
});
|
|
100
117
|
const handleMessageSerially = (0, serialized_handler_1.createSerializedHandler)(handleMessage);
|
|
101
118
|
node_process_1.default.on("message", (msg) => {
|
|
102
119
|
void handleMessageSerially(msg);
|
|
103
120
|
});
|
|
104
121
|
node_process_1.default.on("uncaughtException", (err) => {
|
|
105
|
-
console.error("[process-worker] uncaughtException", err);
|
|
122
|
+
console.error("[process-worker] uncaughtException", restrictedInFlight ? "restricted query failed" : err);
|
|
106
123
|
node_process_1.default.exitCode = 1;
|
|
107
124
|
node_process_1.default.exit();
|
|
108
125
|
});
|
|
109
126
|
node_process_1.default.on("unhandledRejection", (reason) => {
|
|
110
|
-
console.error("[process-worker] unhandledRejection", reason);
|
|
127
|
+
console.error("[process-worker] unhandledRejection", restrictedInFlight ? "restricted query failed" : reason);
|
|
111
128
|
node_process_1.default.exitCode = 1;
|
|
112
129
|
node_process_1.default.exit();
|
|
113
130
|
});
|
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.isExistingQueryReady = void 0;
|
|
12
13
|
exports.default = processFile;
|
|
13
14
|
exports.encodeQuery = encodeQuery;
|
|
14
15
|
exports.rerank = rerank;
|
|
@@ -33,9 +34,11 @@ function processFile(input, onProgress) {
|
|
|
33
34
|
}
|
|
34
35
|
function encodeQuery(input) {
|
|
35
36
|
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
-
return orchestrator.encodeQuery(input.text);
|
|
37
|
+
return orchestrator.encodeQuery(input.text, input.existingOnly, input.generation);
|
|
37
38
|
});
|
|
38
39
|
}
|
|
40
|
+
const isExistingQueryReady = () => orchestrator.isExistingQueryReady();
|
|
41
|
+
exports.isExistingQueryReady = isExistingQueryReady;
|
|
39
42
|
function rerank(input) {
|
|
40
43
|
return __awaiter(this, void 0, void 0, function* () {
|
|
41
44
|
return orchestrator.rerank(input);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.71",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"postversion": "bash scripts/postrelease.sh",
|
|
47
47
|
"audit:consumer": "node scripts/audit-consumer.cjs",
|
|
48
48
|
"audit:python": "python3 scripts/audit-python.py && python3 scripts/audit-python.py --lockfile lance-maintenance/uv.lock",
|
|
49
|
-
"test:python": "python3 -m unittest discover -s tests/python"
|
|
49
|
+
"test:python": "python3 -m unittest discover -s tests/python",
|
|
50
|
+
"audit:document-search": "node scripts/audit-document-search.cjs"
|
|
50
51
|
},
|
|
51
52
|
"keywords": [
|
|
52
53
|
"grepmax",
|
|
@@ -79,7 +80,8 @@
|
|
|
79
80
|
"lance-maintenance/uv.lock",
|
|
80
81
|
"README.md",
|
|
81
82
|
"LICENSE",
|
|
82
|
-
"NOTICE"
|
|
83
|
+
"NOTICE",
|
|
84
|
+
"DOCUMENT-SEARCH.md"
|
|
83
85
|
],
|
|
84
86
|
"license": "Apache-2.0",
|
|
85
87
|
"description": "Semantic code search for coding agents. Local embeddings, call graph tracing, compact code context.",
|