grepmax 0.26.71 → 0.26.72

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.
@@ -10,7 +10,7 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.26.71",
13
+ "version": "0.26.72",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -63,16 +63,37 @@ confirmation of the existing configured MLX model. Idle time alone does not revo
63
63
  that identity: each bounded `/embed` response revalidates model and dimensions,
64
64
  and successful inference renews the normal health cache. Failure revokes restricted
65
65
  eligibility until external work confirms readiness. A worker rechecks before inference;
66
- no initialization, health polling, retry or backend fallback is allowed. Cold or
67
- busy workers refuse immediately without queueing, spawning or scaling.
66
+ no initialization, health polling, retry or backend fallback is allowed. Cold
67
+ workers refuse immediately. When compatible warm workers are occupied, at most
68
+ four document queries may wait FIFO for up to two seconds for a warm worker.
69
+ They get a turn between indexing files; after four consecutive document-query
70
+ assignments, waiting ordinary work gets a turn. A full queue or expired worker
71
+ wait returns `busy`. `queryState: ready` describes admission availability, including
72
+ this bounded wait, and does not guarantee immediate execution or query success.
73
+ Waiting requests live outside the ordinary worker queues and cannot trigger
74
+ worker startup, replacement or scaling. Existing ordinary work retains its own
75
+ admitted lifecycle, including independent expansion/replacement.
68
76
 
69
- Read-only admission samples existing host/ledger state without creating locks,
70
- reservations, pruning records or changing containment markers. Existing Darwin
71
- inference requires known-normal aggregate resources even if ordinary expansion
72
- uses a looser policy. Other platforms retain the existing no-footprint-probe
73
- policy; this is not a cross-platform native qualification claim.
74
- An absent ledger in a fresh `critical-only` home is sampled as empty without being
75
- created; existing corrupt or symlinked ledgers still refuse. Markdown coverage
77
+ Admission and warm readiness are rechecked when a worker becomes available.
78
+ Queued cancellation/deadlines withdraw the request; cancellation after dispatch
79
+ keeps the shared worker assigned until its actual terminal response or exit.
80
+ The ten-second request bound includes waiting rather than restarting at dispatch.
81
+ Synchronous admission has its own probe bound and is checked against the absolute
82
+ request deadline before any query is sent. Native retrieval still uses the
83
+ remaining overall deadline. An in-flight indexing file is never interrupted;
84
+ long files or backend outages can still exceed the worker wait and return `busy`.
85
+
86
+ Read-only admission honors the configured host guard policy without creating locks,
87
+ reservations, pruning records or changing containment markers. Under the default
88
+ `strict` policy, existing Darwin inference requires fresh known-normal aggregate
89
+ resources and reads the existing ledger; absent ledgers are treated as empty and
90
+ corrupt or symlinked ledgers refuse. Explicit `critical-only` uses fresh OS/kernel
91
+ pressure probes without aggregate sampling or ledger access: warning/unknown
92
+ measurements remain diagnostic, while confirmed critical pressure refuses.
93
+ Containment is checked before and after sampling in either policy. This read-only
94
+ path never creates a safety latch; the daemon's safety machinery remains authoritative.
95
+ Other platforms retain the existing no-footprint-probe policy; this is not a
96
+ cross-platform native qualification claim. Markdown coverage
76
97
  and returned pointers require regular files, including after symlink resolution;
77
98
  a directory whose name ends in `.md` can be a prefix but never a source pointer.
78
99
 
package/README.md CHANGED
@@ -241,6 +241,19 @@ index and already-warm compatible query worker. It cannot start a daemon, watch
241
241
  project, initialize a model, fall back to another backend or mutate the index.
242
242
  Cold, busy or unavailable resources return an explicit unavailable state.
243
243
 
244
+ Document query admission follows the configured host guard policy. On macOS,
245
+ default `strict` requires fresh aggregate resource admission; explicit
246
+ `critical-only` checks OS/kernel critical pressure without an aggregate scan or
247
+ ledger access. Warning or unavailable measurements stay diagnostic in that mode.
248
+ Both policies preserve containment checks. Up to four document queries can wait
249
+ FIFO for an already-warm compatible worker for up to two seconds, taking a turn
250
+ between indexing files. After four consecutive document queries, waiting ordinary
251
+ work gets a turn. Saturation or wait expiry returns `busy`; cold workers still
252
+ refuse. This wait cannot start, replace or scale workers, and it never interrupts
253
+ an in-flight file. Admission/readiness are checked again before dispatch, and the
254
+ overall ten-second deadline includes waiting. Readiness means a request can be
255
+ admitted, including this bounded wait. Long indexing files can still return `busy`.
256
+
244
257
  Results contain canonical paths and one-based source ranges, without cached source
245
258
  bodies. The first slice uses dense retrieval with explicit path scope and bounded
246
259
  deadlines. Consumers must reread and authorize current source before using a
@@ -594,6 +607,75 @@ The candidate trace contains pointers and stage ranks for at most the first 200
594
607
 
595
608
  Each traced candidate also has an `outcome`: returned, stage-one/pooled cut, deduplicated, per-file limit or display limit. Display limit means the production loop filled its result window before examining that candidate; it does not assert that the candidate would pass the per-file cap.
596
609
 
610
+ #### Existing-index document retrieval evaluation
611
+
612
+ In a source checkout, [scripts/eval-documents.cjs](scripts/eval-documents.cjs) measures the released [document MCP contract](DOCUMENT-SEARCH.md) through an explicit installed entry point. It uses only Node built-ins and sequential `document_search_status` / `semantic_search` calls. It performs no model warmup, daemon startup, indexing, refusal retries or fallback to normal hybrid search.
613
+
614
+ ```bash
615
+ node scripts/eval-documents.cjs \
616
+ --fixture docs/measurements/documents/fixture-v1.json \
617
+ --sha256 <reviewed-fixture-digest> \
618
+ --root /absolute/already-indexed/project \
619
+ --entry /absolute/installed/grepmax/dist/bin.js \
620
+ --output docs/measurements/documents/baseline-v1.json
621
+ ```
622
+
623
+ Freeze the JSON fixture before retrieving results. It contains `schemaVersion: 1`, `purpose`, explicit project-relative `prefixes`, and 1–100 `cases`. Each case has a unique `id`, a `query` (maximum 500 characters), and one or more `expected` targets: `{file, startLine, endLine, sourceSha256}`. Files are contained relative Markdown paths; ranges are one-based and answer-bearing; hashes cover exact source bytes. Keep private fixtures and evidence in ignored `docs/measurements/`. Optional `--repeats 1-3` repeats the same frozen cases without retries or warmup.
624
+
625
+ The report separates the rank of the expected document among distinct returned documents from the rank of an overlapping expected section among chunk pointers. Document Recall@10 credits the fraction of declared target documents retrieved; MRR@10 credits only ranks 1–10. Section overlap indicates source-context discovery, not complete answer coverage. Only the first 50 returned candidates are available; document deduplication cannot recover targets outside that window. Reviewed targets are not exhaustive judgments of all useful documents, so these metrics describe the frozen targets, not general recall.
626
+
627
+ Source and indexed hashes must match the fixture before querying; generation and project/store identity must agree across coverage and retrieval. Returned source ranges require reread/hash verification; new artifacts retain both the indexed pointer digest and current-source digest so later reviews can check provenance. Refusals, changed targets and unverified expected pointers are excluded from relevance metrics and counted separately; `usableFraction` includes every planned sample. Background indexing and partial/degraded index context are recorded without excluding targets whose coverage and hashes remain current. No queries, source bodies or raw diagnostics are copied into result artifacts. Reads are bounded to 2 MiB per source; MCP frames to 1 MiB. The report and sibling `.samples.jsonl` checkpoint refuse overwrite and use owner-only permissions. Exit 0 means an evaluation with usable samples completed, not that a quality threshold passed; exit 2 means no usable samples; exit 1 means preflight or transport/contract failure. No consumer rollout is implied.
628
+
629
+ To investigate scope competition, [scripts/eval-document-scopes.cjs](scripts/eval-document-scopes.cjs) compares the original fixture prefixes against declared narrower directories without changing queries or targets:
630
+
631
+ ```bash
632
+ node scripts/eval-document-scopes.cjs \
633
+ --fixture docs/measurements/documents/fixture-v1.json --sha256 <digest> \
634
+ --root /absolute/already-indexed/project \
635
+ --entry /absolute/installed/grepmax/dist/bin.js \
636
+ --output docs/measurements/documents/paired-scope-v1.json \
637
+ --narrow-prefix docs/services --narrow-prefix docs/modules
638
+ ```
639
+
640
+ Declare and freeze the scope hypothesis before querying. Narrower prefixes must stay within the original scope and retain every target. Arm order alternates per case, with a one-second gap between pairs and no retries. Reports show each arm's standalone observations and separately compare only pairs where both arms are usable under the same resource generation, project and store. The live competing corpus can still change; this is a diagnostic comparison rather than an isolated causal test. Scopes chosen from known target families introduce a useful prior, so gains do not establish unscoped or held-out retrieval quality. Owner-only checkpoints, overwrite refusal and source/transport bounds are shared with the base evaluator. Exit 2 means no comparable pairs.
641
+
642
+ ### Offline lexical candidate experiment
643
+
644
+ [scripts/eval-document-lexical.cjs](scripts/eval-document-lexical.cjs) tests lexical candidates and fixed document reciprocal-rank fusion against a prior paired scope report. It uses Node built-ins and filesystem Markdown only; it never contacts a daemon, opens a native store, loads a model or changes production retrieval.
645
+
646
+ Freeze an owner-only study plan before scoring, including the original fixture checksum, archived report checksum, canonical root, narrower relative prefixes, fixed algorithm parameters and resource limits. The selected scope must retain every target and match the archived narrow arm. Pass that plan's checksum explicitly:
647
+
648
+ ```bash
649
+ NODE_OPTIONS=--max-old-space-size=384 node scripts/eval-document-lexical.cjs \
650
+ --fixture docs/measurements/documents/fixture-v1.json \
651
+ --plan docs/measurements/documents/lexical-plan.json --plan-sha256 <digest> \
652
+ --archive docs/measurements/documents/paired-scope-v1.json \
653
+ --snapshot docs/measurements/documents/new-private-snapshot \
654
+ --output docs/measurements/documents/new-lexical-report.json
655
+ ```
656
+
657
+ The prototype uses fixed 48-line windows with eight-line overlap, BM25 `k1=1.2` / `b=0.75`, 50 positive-score lexical candidates and equal-weight document RRF with constant 60. Targets are used for grading only. Stable path/line ties, no per-case overrides and no zero-score padding keep the experiment reproducible. Bounds are 512 files, 16 MiB total source, 2 MiB per file, 10,000 windows, one million token occurrences and 20,000 directory entries. Missing, aliased, changed or oversized source fails snapshot creation rather than silently dropping competing documents. A private snapshot preserves exact source bytes and a digest manifest; the report stores pointers, digests, IDs and scores without questions or source bodies. Existing output/snapshot paths are refused.
658
+
659
+ Lexical metrics cover cases whose frozen targets match the snapshot. Archive replay additionally requires original ready state/resource identity and every competitor pointer's indexed/current digest and range to match the snapshot; an invalid competitor excludes the entire case. Refused archive cases stay separate. Offline lexical availability does not exercise deployed busy/pressure admission. Archived dense candidates are not re-encoded over this snapshot, and narrower families chosen from known target locations introduce a prior. Report document gains/losses on the verified shared replay cohort; section overlap is diagnostic because window sizes and hybrid representatives differ from production chunks. A passing exploratory gate only supports proposing further matched qualification. No production integration or rollout follows automatically. Exit 0 means scoring completed with usable lexical cases, even when the gate fails; exit 2 means no usable lexical cases, and exit 1 means preflight or evaluation failure.
660
+
661
+ ### Protected document fusion qualification
662
+
663
+ [scripts/eval-document-fusion.cjs](scripts/eval-document-fusion.cjs) qualifies a fixed candidate ordering in two modes: replay cached verified lexical/dense candidates, or collect one broader pass through the released existing-only MCP. It interleaves each arm's first three distinct documents (`lex1,dense1,lex2,dense2,lex3,dense3`), deduplicates while preserving the selected arm's pointer, then fills to 50 with the fixed RRF tail. This protects high-ranked lexical-only evidence; spending up to six slots can displace useful lower-ranked dense candidates. Expected targets never affect ranking.
664
+
665
+ ```bash
666
+ NODE_OPTIONS=--max-old-space-size=384 node scripts/eval-document-fusion.cjs \
667
+ --mode live --design <frozen-design.json> --design-sha256 <digest> \
668
+ --fixture <frozen-fixture.json> --fixture-sha256 <digest> \
669
+ --entry /absolute/path/to/installed/grepmax/dist/bin.js --entry-sha256 <digest> \
670
+ --snapshot <new-private-snapshot-directory> --output <new-private-report.json>
671
+ ```
672
+
673
+ For `--mode replay`, replace the entry arguments with `--input <prior-lexical-report.json> --input-sha256 <digest>` and point `--snapshot` at its immutable snapshot. Replay uses saved lexical candidates without rescoring queries. Live mode snapshots the complete predefined scope and builds the bounded filesystem index before MCP initialization; it permits one case-order pass with one-second gaps, without retries, warmup, runtime startup or native fallback. The existing-only entry must expose the expected restricted capabilities. Coverage, generation, project/store identity and every indexed/current competitor digest and range must verify. Refusals remain separate from relevance. A final full source-byte/file-set fence invalidates comparisons if the corpus changed; checkpoints are provisional and the final JSON is authoritative.
674
+
675
+ The lexical CLI retains its one-million-token default. A frozen fusion design may explicitly allow 1,100,000 token occurrences; all other source/window limits and the 384 MiB Node heap setting remain unchanged. The heap setting is not an operating-system physical memory cap. Outputs and snapshots refuse overwrite and use private permissions; reports exclude question text and source bodies. The broader study requires at least 16 of 20 comparable cases, no dense top-ten target losses, and protected document recall at least as high as either arm on the same cohort. Other predefined positive cohort bounds are supported. Exit 0 means collection completed with some comparable cases, even if the gate fails; exit 2 means none, and exit 1 means interrupted/preflight failure.
676
+
677
+ The October 8 experiment recovered both original residuals: Redis rank 1, People rank 3, 15/15 replay document hits with no dense top-ten target loss. Its broader 393-file, 20-case pass produced only two comparable cases (15 busy and three pressure refusals; one busy case also lacked indexed coverage), below the frozen 16-case minimum. Broader qualification is **insufficient**. Questions were source-curated after the rule freeze and held out from parameter selection, not independently adjudicated real-user questions. Representative/window differences prevent like-for-like section accuracy claims. This evaluation does not change production retrieval or authorize integration, release or rollout.
678
+
597
679
  ## Attribution
598
680
 
599
681
  grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
@@ -323,12 +323,22 @@ class ResourceBudget {
323
323
  },
324
324
  };
325
325
  }
326
- /** Read-only admission for existing inference. Never seed/lock/prune the ledger or latch. */
326
+ /** Read-only admission for existing inference, honoring the selected host policy.
327
+ * Never seed/lock/prune the ledger or latch. */
327
328
  checkExisting() {
328
329
  if (this.deps.quarantine())
329
330
  throw new ResourceAdmissionError("host containment");
330
331
  if (this.deps.platform !== "darwin")
331
332
  return null;
333
+ if (this.deps.policy() === "critical-only") {
334
+ const snapshot = this.deps.criticalSample();
335
+ if (snapshot.memoryPressure === "critical" ||
336
+ snapshot.kernelPressure === "critical")
337
+ throw new ResourceAdmissionError("critical host pressure; existing inference refused", true);
338
+ if (this.deps.quarantine())
339
+ throw new ResourceAdmissionError("host containment");
340
+ return snapshot;
341
+ }
332
342
  const records = this.readExistingRecords();
333
343
  const snapshot = this.deps.sample();
334
344
  assertResourceSnapshot(snapshot, this.charge(snapshot, records), resolveResourceBudgetMb(), this.deps.now(), this.deps.sampleMaxAgeMs);
@@ -42,7 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.WorkerPool = void 0;
45
+ exports.WorkerPool = exports.EXISTING_QUERY_QUEUE_LIMIT = exports.EXISTING_QUERY_WAIT_MS = void 0;
46
46
  exports.embeddingEnv = embeddingEnv;
47
47
  exports.getWorkerPool = getWorkerPool;
48
48
  exports.destroyWorkerPool = destroyWorkerPool;
@@ -63,6 +63,9 @@ const kernel_zone_1 = require("../utils/kernel-zone");
63
63
  const logger_1 = require("../utils/logger");
64
64
  const resource_budget_1 = require("../utils/resource-budget");
65
65
  const safety_latch_1 = require("../utils/safety-latch");
66
+ exports.EXISTING_QUERY_WAIT_MS = 2000;
67
+ exports.EXISTING_QUERY_QUEUE_LIMIT = 4;
68
+ const EXISTING_QUERY_DEADLINE_MS = 10000;
66
69
  const TASK_TIMEOUT_MS = (() => {
67
70
  var _a;
68
71
  const fromEnv = Number.parseInt((_a = process.env.GMAX_WORKER_TASK_TIMEOUT_MS) !== null && _a !== void 0 ? _a : "", 10);
@@ -249,6 +252,9 @@ class WorkerPool {
249
252
  this.priorityQueue = [];
250
253
  this.taskQueue = [];
251
254
  this.tasks = new Map();
255
+ // Separate from ordinary queues: these requests must never trigger expansion.
256
+ this.existingWaiters = new Map();
257
+ this.existingBurst = 0;
252
258
  this.nextId = 1;
253
259
  this.destroyed = false;
254
260
  this.destroyPromise = null;
@@ -298,6 +304,10 @@ class WorkerPool {
298
304
  : [{ pid: worker.child.pid, rssBytes: worker.lastRssBytes }]);
299
305
  }
300
306
  clearTaskTimeout(task) {
307
+ if (task.waitTimeout) {
308
+ clearTimeout(task.waitTimeout);
309
+ task.waitTimeout = undefined;
310
+ }
301
311
  if (task.timeout) {
302
312
  clearTimeout(task.timeout);
303
313
  task.timeout = undefined;
@@ -352,6 +362,7 @@ class WorkerPool {
352
362
  task.abortListener = undefined;
353
363
  }
354
364
  this.tasks.delete(task.id);
365
+ this.existingWaiters.delete(task.id);
355
366
  this.removeFromQueue(task.id);
356
367
  if (worker) {
357
368
  worker.busy = false;
@@ -617,19 +628,25 @@ class WorkerPool {
617
628
  if (this.destroyed)
618
629
  return;
619
630
  if (this.spawnDeniedReason) {
631
+ this.rejectExistingWaiters("host_pressure");
620
632
  this.rejectUnassignedTasks(this.spawnDeniedReason, "HOST_SAFETY");
621
633
  return;
622
634
  }
623
635
  // Work admitted before another process quarantines the host may still
624
636
  // be queued. Do not send it to an existing worker after that stop.
625
637
  try {
638
+ this.dispatchExistingWaiters();
639
+ if (this.destroyed)
640
+ return;
626
641
  const quarantine = (0, autostart_1.daemonStartDeniedReason)();
627
642
  if (quarantine) {
643
+ this.rejectExistingWaiters("host_pressure");
628
644
  this.denySpawn(quarantine, false);
629
645
  return;
630
646
  }
631
647
  }
632
648
  catch (_f) {
649
+ this.rejectExistingWaiters("host_pressure");
633
650
  this.denySpawn("quarantine check failed");
634
651
  return;
635
652
  }
@@ -681,6 +698,7 @@ class WorkerPool {
681
698
  return;
682
699
  }
683
700
  idle.suppressReplacement = false;
701
+ this.existingBurst = 0;
684
702
  idle.busy = true;
685
703
  idle.pendingTaskId = task.id;
686
704
  idle.busySince = Date.now();
@@ -752,8 +770,7 @@ class WorkerPool {
752
770
  processFile(input, signal) {
753
771
  return this.enqueue("processFile", input, signal);
754
772
  }
755
- /** No queue, scaling or initialization for an existing-index document query. */
756
- existingQueryState() {
773
+ existingWorkerState() {
757
774
  if (this.destroyed || this.spawnDeniedReason || (0, autostart_1.daemonStartDeniedReason)())
758
775
  return "host_pressure";
759
776
  const warm = this.workers.filter((w) => w.queryReady && !w.cleanedUp && w.child.connected !== false);
@@ -763,23 +780,112 @@ class WorkerPool {
763
780
  ? "busy"
764
781
  : "embedding_unavailable";
765
782
  }
783
+ /** Admission readiness includes a short bounded wait for an already-warm worker. */
784
+ existingQueryState() {
785
+ const state = this.existingWorkerState();
786
+ return state === "busy" &&
787
+ this.existingWaiters.size < exports.EXISTING_QUERY_QUEUE_LIMIT
788
+ ? "ready"
789
+ : this.existingWaiters.size >= exports.EXISTING_QUERY_QUEUE_LIMIT &&
790
+ state === "ready"
791
+ ? "busy"
792
+ : state;
793
+ }
794
+ rejectExistingWaiters(state) {
795
+ for (const task of this.existingWaiters.values()) {
796
+ task.reject(Object.assign(new Error(state), { code: state }));
797
+ this.completeTask(task, null);
798
+ }
799
+ }
800
+ dispatchExistingWaiters() {
801
+ while (this.existingWaiters.size) {
802
+ const state = this.existingWorkerState();
803
+ if (state === "host_pressure" || state === "embedding_unavailable") {
804
+ this.rejectExistingWaiters(state);
805
+ return;
806
+ }
807
+ // A continuous caller stream cannot keep indexing behind this queue.
808
+ if (this.existingBurst >= exports.EXISTING_QUERY_QUEUE_LIMIT &&
809
+ [...this.tasks.values()].some((t) => !t.worker && !t.existingOnly))
810
+ return;
811
+ if (state === "busy")
812
+ return;
813
+ const task = this.existingWaiters.values().next().value;
814
+ const age = Date.now() - task.queuedAt;
815
+ if (age < 0 || age >= exports.EXISTING_QUERY_WAIT_MS) {
816
+ task.reject(Object.assign(new Error("busy"), { code: "busy" }));
817
+ this.completeTask(task, null);
818
+ continue;
819
+ }
820
+ // The worker wait ends here; synchronous admission has its own bound and
821
+ // the request's overall timer/signal still applies during inference.
822
+ if (task.waitTimeout)
823
+ clearTimeout(task.waitTimeout);
824
+ task.waitTimeout = undefined;
825
+ try {
826
+ resource_budget_1.resourceBudget.checkExisting();
827
+ }
828
+ catch (_a) {
829
+ this.rejectExistingWaiters("host_pressure");
830
+ // Ordinary work performs its own admission; a document refusal must
831
+ // not strand that queue. The dispatcher rechecks containment next.
832
+ return;
833
+ }
834
+ // Cancellation during admission may have removed this waiter.
835
+ if (!this.existingWaiters.has(task.id))
836
+ continue;
837
+ const admittedAge = Date.now() - task.queuedAt;
838
+ if (admittedAge < 0 || admittedAge >= EXISTING_QUERY_DEADLINE_MS) {
839
+ task.reject(new Error("timeout"));
840
+ this.completeTask(task, null);
841
+ continue;
842
+ }
843
+ const after = this.existingWorkerState();
844
+ if (after !== "ready") {
845
+ if (after === "busy") {
846
+ task.reject(Object.assign(new Error("busy"), { code: "busy" }));
847
+ this.completeTask(task, null);
848
+ continue;
849
+ }
850
+ this.rejectExistingWaiters(after);
851
+ return;
852
+ }
853
+ const worker = this.workers.find((w) => w.queryReady &&
854
+ !w.busy &&
855
+ !w.cleanedUp &&
856
+ w.child.connected !== false);
857
+ this.existingWaiters.delete(task.id);
858
+ if (task.waitTimeout)
859
+ clearTimeout(task.waitTimeout);
860
+ task.waitTimeout = undefined;
861
+ task.worker = worker;
862
+ task.startTime = Date.now();
863
+ worker.busy = true;
864
+ worker.suppressReplacement = true;
865
+ worker.busySince = Date.now();
866
+ worker.pendingTaskId = task.id;
867
+ this.tasks.set(task.id, task);
868
+ this.existingBurst++;
869
+ try {
870
+ worker.child.send({
871
+ id: task.id,
872
+ method: task.method,
873
+ payload: task.payload,
874
+ });
875
+ }
876
+ catch (_b) {
877
+ this.completeTask(task, worker);
878
+ worker.queryReady = false;
879
+ task.reject(new Error("embedding_unavailable"));
880
+ }
881
+ }
882
+ }
766
883
  encodeQueryExisting(text, signal) {
767
884
  const state = this.existingQueryState();
768
885
  if (state !== "ready")
769
886
  return Promise.reject(Object.assign(new Error(state), { code: state }));
770
887
  if (signal === null || signal === void 0 ? void 0 : signal.aborted)
771
888
  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
889
  return new Promise((resolve, reject) => {
784
890
  const id = this.nextId++;
785
891
  const task = {
@@ -795,11 +901,14 @@ class WorkerPool {
795
901
  queuedAt: Date.now(),
796
902
  signal,
797
903
  existingOnly: true,
798
- worker,
799
904
  };
800
905
  const cancel = () => {
801
906
  task.callerAborted = true;
802
907
  reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
908
+ if (!task.worker) {
909
+ this.completeTask(task, null);
910
+ this.dispatch();
911
+ }
803
912
  };
804
913
  task.abortListener = cancel;
805
914
  signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", cancel, { once: true });
@@ -808,20 +917,16 @@ class WorkerPool {
808
917
  task.timeout = setTimeout(() => {
809
918
  task.callerAborted = true;
810
919
  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
- }
920
+ if (!task.worker)
921
+ this.completeTask(task, null);
922
+ }, EXISTING_QUERY_DEADLINE_MS);
923
+ task.waitTimeout = setTimeout(() => {
924
+ reject(Object.assign(new Error("busy"), { code: "busy" }));
925
+ this.completeTask(task, null);
926
+ this.dispatch();
927
+ }, exports.EXISTING_QUERY_WAIT_MS);
928
+ this.existingWaiters.set(id, task);
929
+ this.dispatch();
825
930
  });
826
931
  }
827
932
  encodeQuery(text, signal) {
@@ -1006,6 +1111,7 @@ class WorkerPool {
1006
1111
  this.idleReapInterval = null;
1007
1112
  }
1008
1113
  this.clearScaleUpTimer();
1114
+ this.rejectExistingWaiters("embedding_unavailable");
1009
1115
  for (const task of this.tasks.values()) {
1010
1116
  task.reject(new Error("Worker pool destroyed"));
1011
1117
  this.completeTask(task, null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.71",
3
+ "version": "0.26.72",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.71",
3
+ "version": "0.26.72",
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",