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.
@@ -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
- encodeQuery(text) {
327
- return __awaiter(this, void 0, void 0, function* () {
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
- yield this.ensureReady();
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 = yield (0, mlx_client_1.mlxEmbed)([text], {
332
- mode: this.embedMode,
333
- expectedModel: this.generation.mlxModel,
334
- expectedDim: this.generation.vectorDim,
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, [
@@ -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 || hasPendingTasks) {
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 (!this.destroyed && this.workers.length < MIN_KEEP_WORKERS) {
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 message = err instanceof Error ? err.message : String(err);
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 = err === null || err === void 0 ? void 0 : err.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.70",
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.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.70",
3
+ "version": "0.26.71",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",