grepmax 0.21.3 → 0.23.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 +2 -4
- package/README.md +2 -1
- package/dist/commands/audit.js +111 -5
- package/dist/commands/dead.js +2 -1
- package/dist/commands/impact.js +3 -3
- package/dist/commands/mcp.js +29 -9
- package/dist/commands/peek.js +17 -4
- package/dist/commands/plugin.js +34 -24
- package/dist/commands/search-run.js +2 -0
- package/dist/commands/search.js +21 -11
- package/dist/commands/test-find.js +2 -2
- package/dist/commands/trace.js +13 -5
- package/dist/lib/daemon/ipc-handler.js +1 -0
- package/dist/lib/daemon/mlx-server-manager.js +52 -8
- package/dist/lib/daemon/process-manager.js +16 -17
- package/dist/lib/graph/graph-builder.js +92 -13
- package/dist/lib/graph/impact.js +66 -16
- package/dist/lib/index/batch-processor.js +45 -9
- package/dist/lib/llm/tools.js +3 -3
- package/dist/lib/search/searcher.js +26 -21
- package/dist/lib/store/vector-db.js +20 -6
- package/dist/lib/utils/daemon-client.js +35 -9
- package/dist/lib/workers/pool.js +62 -20
- package/package.json +9 -4
- package/plugins/grepmax/.claude-plugin/plugin.json +2 -8
- package/scripts/postinstall.js +8 -115
|
@@ -63,6 +63,7 @@ class ProjectBatchProcessor {
|
|
|
63
63
|
this.pending = new Map();
|
|
64
64
|
this.retryCount = new Map();
|
|
65
65
|
this.debounceTimer = null;
|
|
66
|
+
this.activeBatch = null;
|
|
66
67
|
this.processing = false;
|
|
67
68
|
this.closed = false;
|
|
68
69
|
this.currentBatchAc = null;
|
|
@@ -105,14 +106,34 @@ class ProjectBatchProcessor {
|
|
|
105
106
|
var _a;
|
|
106
107
|
this.closed = true;
|
|
107
108
|
(_a = this.currentBatchAc) === null || _a === void 0 ? void 0 : _a.abort();
|
|
108
|
-
if (this.debounceTimer)
|
|
109
|
+
if (this.debounceTimer) {
|
|
109
110
|
clearTimeout(this.debounceTimer);
|
|
111
|
+
this.debounceTimer = null;
|
|
112
|
+
}
|
|
113
|
+
if (this.activeBatch) {
|
|
114
|
+
yield this.activeBatch;
|
|
115
|
+
}
|
|
110
116
|
});
|
|
111
117
|
}
|
|
112
118
|
scheduleBatch() {
|
|
113
119
|
if (this.debounceTimer)
|
|
114
120
|
clearTimeout(this.debounceTimer);
|
|
115
|
-
this.debounceTimer = setTimeout(() =>
|
|
121
|
+
this.debounceTimer = setTimeout(() => {
|
|
122
|
+
this.debounceTimer = null;
|
|
123
|
+
this.startBatch();
|
|
124
|
+
}, DEBOUNCE_MS);
|
|
125
|
+
}
|
|
126
|
+
startBatch() {
|
|
127
|
+
if (this.activeBatch)
|
|
128
|
+
return;
|
|
129
|
+
const run = this.processBatch().catch((err) => {
|
|
130
|
+
console.error(`[${this.wtag}] Batch processing failed:`, err);
|
|
131
|
+
});
|
|
132
|
+
this.activeBatch = run;
|
|
133
|
+
void run.finally(() => {
|
|
134
|
+
if (this.activeBatch === run)
|
|
135
|
+
this.activeBatch = null;
|
|
136
|
+
});
|
|
116
137
|
}
|
|
117
138
|
processBatch() {
|
|
118
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -124,7 +145,10 @@ class ProjectBatchProcessor {
|
|
|
124
145
|
(0, logger_1.log)(this.wtag, "Disk critically low — deferring batch processing");
|
|
125
146
|
if (this.debounceTimer)
|
|
126
147
|
clearTimeout(this.debounceTimer);
|
|
127
|
-
this.debounceTimer = setTimeout(() =>
|
|
148
|
+
this.debounceTimer = setTimeout(() => {
|
|
149
|
+
this.debounceTimer = null;
|
|
150
|
+
this.startBatch();
|
|
151
|
+
}, 60000);
|
|
128
152
|
return;
|
|
129
153
|
}
|
|
130
154
|
this.processing = true;
|
|
@@ -158,11 +182,10 @@ class ProjectBatchProcessor {
|
|
|
158
182
|
const vectors = [];
|
|
159
183
|
const metaUpdates = new Map();
|
|
160
184
|
const metaDeletes = [];
|
|
161
|
-
const
|
|
185
|
+
const completed = new Set();
|
|
162
186
|
for (const [absPath, event] of batch) {
|
|
163
187
|
if (batchAc.signal.aborted)
|
|
164
188
|
break;
|
|
165
|
-
attempted.add(absPath);
|
|
166
189
|
processed++;
|
|
167
190
|
if (batch.size > 10 &&
|
|
168
191
|
(processed % 10 === 0 || processed === batch.size)) {
|
|
@@ -172,6 +195,7 @@ class ProjectBatchProcessor {
|
|
|
172
195
|
deletes.push(absPath);
|
|
173
196
|
metaDeletes.push(absPath);
|
|
174
197
|
reindexed++;
|
|
198
|
+
completed.add(absPath);
|
|
175
199
|
continue;
|
|
176
200
|
}
|
|
177
201
|
// change or add
|
|
@@ -186,10 +210,12 @@ class ProjectBatchProcessor {
|
|
|
186
210
|
metaDeletes.push(absPath);
|
|
187
211
|
reindexed++;
|
|
188
212
|
}
|
|
213
|
+
completed.add(absPath);
|
|
189
214
|
continue;
|
|
190
215
|
}
|
|
191
216
|
const cached = this.metaCache.get(absPath);
|
|
192
217
|
if ((0, cache_check_1.isFileCached)(cached, stats)) {
|
|
218
|
+
completed.add(absPath);
|
|
193
219
|
continue;
|
|
194
220
|
}
|
|
195
221
|
// Fast path: if only mtime changed but size matches and we have a hash,
|
|
@@ -199,6 +225,7 @@ class ProjectBatchProcessor {
|
|
|
199
225
|
const hash = (0, file_utils_1.computeContentHash)(buf, absPath);
|
|
200
226
|
if (hash === cached.hash) {
|
|
201
227
|
metaUpdates.set(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: stats.mtimeMs }));
|
|
228
|
+
completed.add(absPath);
|
|
202
229
|
continue;
|
|
203
230
|
}
|
|
204
231
|
}
|
|
@@ -213,12 +240,14 @@ class ProjectBatchProcessor {
|
|
|
213
240
|
};
|
|
214
241
|
if (cached && cached.hash === result.hash) {
|
|
215
242
|
metaUpdates.set(absPath, metaEntry);
|
|
243
|
+
completed.add(absPath);
|
|
216
244
|
continue;
|
|
217
245
|
}
|
|
218
246
|
if (result.shouldDelete) {
|
|
219
247
|
deletes.push(absPath);
|
|
220
248
|
metaUpdates.set(absPath, metaEntry);
|
|
221
249
|
reindexed++;
|
|
250
|
+
completed.add(absPath);
|
|
222
251
|
continue;
|
|
223
252
|
}
|
|
224
253
|
deletes.push(absPath);
|
|
@@ -227,6 +256,7 @@ class ProjectBatchProcessor {
|
|
|
227
256
|
}
|
|
228
257
|
metaUpdates.set(absPath, metaEntry);
|
|
229
258
|
reindexed++;
|
|
259
|
+
completed.add(absPath);
|
|
230
260
|
}
|
|
231
261
|
catch (err) {
|
|
232
262
|
if (batchAc.signal.aborted)
|
|
@@ -236,6 +266,7 @@ class ProjectBatchProcessor {
|
|
|
236
266
|
deletes.push(absPath);
|
|
237
267
|
metaDeletes.push(absPath);
|
|
238
268
|
reindexed++;
|
|
269
|
+
completed.add(absPath);
|
|
239
270
|
}
|
|
240
271
|
else {
|
|
241
272
|
console.error(`[${this.wtag}] Failed to process ${absPath}:`, err);
|
|
@@ -243,12 +274,14 @@ class ProjectBatchProcessor {
|
|
|
243
274
|
console.error(`[${this.wtag}] Worker pool unhealthy, aborting batch`);
|
|
244
275
|
break;
|
|
245
276
|
}
|
|
277
|
+
completed.add(absPath);
|
|
246
278
|
}
|
|
247
279
|
}
|
|
248
280
|
}
|
|
249
|
-
// Requeue files that
|
|
281
|
+
// Requeue files that didn't reach a terminal outcome. This includes the
|
|
282
|
+
// file whose worker was in flight when the batch was aborted.
|
|
250
283
|
for (const [absPath, event] of batch) {
|
|
251
|
-
if (!
|
|
284
|
+
if (!completed.has(absPath) && !this.pending.has(absPath)) {
|
|
252
285
|
this.pending.set(absPath, event);
|
|
253
286
|
}
|
|
254
287
|
}
|
|
@@ -339,11 +372,14 @@ class ProjectBatchProcessor {
|
|
|
339
372
|
clearTimeout(batchTimeout);
|
|
340
373
|
this.currentBatchAc = null;
|
|
341
374
|
this.processing = false;
|
|
342
|
-
if (this.pending.size > 0) {
|
|
375
|
+
if (!this.closed && this.pending.size > 0) {
|
|
343
376
|
if (backoffOverrideMs > 0) {
|
|
344
377
|
if (this.debounceTimer)
|
|
345
378
|
clearTimeout(this.debounceTimer);
|
|
346
|
-
this.debounceTimer = setTimeout(() =>
|
|
379
|
+
this.debounceTimer = setTimeout(() => {
|
|
380
|
+
this.debounceTimer = null;
|
|
381
|
+
this.startBatch();
|
|
382
|
+
}, backoffOverrideMs);
|
|
347
383
|
}
|
|
348
384
|
else {
|
|
349
385
|
this.scheduleBatch();
|
package/dist/lib/llm/tools.js
CHANGED
|
@@ -293,15 +293,15 @@ function executeImpact(args, ctx) {
|
|
|
293
293
|
if (!target)
|
|
294
294
|
return "(error: missing target)";
|
|
295
295
|
const depth = clampDepth(args.depth);
|
|
296
|
-
const { symbols, resolvedAsFile } = yield (0, impact_1.resolveTargetSymbols)(target, ctx.vectorDb, ctx.projectRoot);
|
|
296
|
+
const { symbols, resolvedAsFile, symbolFamilies } = yield (0, impact_1.resolveTargetSymbols)(target, ctx.vectorDb, ctx.projectRoot);
|
|
297
297
|
if (symbols.length === 0)
|
|
298
298
|
return "(not found)";
|
|
299
299
|
const excludePaths = resolvedAsFile
|
|
300
300
|
? new Set([path.resolve(ctx.projectRoot, target)])
|
|
301
301
|
: undefined;
|
|
302
302
|
const [deps, tests] = yield Promise.all([
|
|
303
|
-
(0, impact_1.findDependents)(symbols, ctx.vectorDb, ctx.projectRoot, excludePaths),
|
|
304
|
-
(0, impact_1.findTests)(symbols, ctx.vectorDb, ctx.projectRoot, depth),
|
|
303
|
+
(0, impact_1.findDependents)(symbols, ctx.vectorDb, ctx.projectRoot, excludePaths, undefined, undefined, symbolFamilies),
|
|
304
|
+
(0, impact_1.findTests)(symbols, ctx.vectorDb, ctx.projectRoot, depth, undefined, symbolFamilies),
|
|
305
305
|
]);
|
|
306
306
|
if (deps.length === 0 && tests.length === 0)
|
|
307
307
|
return "(no impact detected)";
|
|
@@ -374,7 +374,7 @@ class Searcher {
|
|
|
374
374
|
}
|
|
375
375
|
search(query, top_k, _search_options, _filters, pathPrefix, intent, signal) {
|
|
376
376
|
return __awaiter(this, void 0, void 0, function* () {
|
|
377
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
377
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
378
378
|
const finalLimit = top_k !== null && top_k !== void 0 ? top_k : 10;
|
|
379
379
|
// ColBERT rerank is opt-in as of v0.17.1. On the 97-case eval it
|
|
380
380
|
// regresses MRR@10 by ~3% and doubles query latency; sweep across
|
|
@@ -413,7 +413,7 @@ class Searcher {
|
|
|
413
413
|
try {
|
|
414
414
|
table = yield this.db.ensureTable();
|
|
415
415
|
}
|
|
416
|
-
catch (
|
|
416
|
+
catch (_r) {
|
|
417
417
|
return { data: [] };
|
|
418
418
|
}
|
|
419
419
|
// Ensure FTS index exists (lazy init, retry periodically on failure)
|
|
@@ -496,7 +496,7 @@ class Searcher {
|
|
|
496
496
|
this.ftsAvailable = true;
|
|
497
497
|
console.warn("[Searcher] Rebuilt FTS index with position support — retry search");
|
|
498
498
|
}
|
|
499
|
-
catch (
|
|
499
|
+
catch (_s) { }
|
|
500
500
|
}
|
|
501
501
|
else {
|
|
502
502
|
console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
|
|
@@ -670,6 +670,10 @@ class Searcher {
|
|
|
670
670
|
if (stage2Candidates.length === 0) {
|
|
671
671
|
return { data: [] };
|
|
672
672
|
}
|
|
673
|
+
const envMaxPerFile = Number.parseInt((_m = process.env.GMAX_MAX_PER_FILE) !== null && _m !== void 0 ? _m : "", 10);
|
|
674
|
+
const MAX_PER_FILE = Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3;
|
|
675
|
+
const displayWindow = Math.min(stage2Candidates.length, Math.max(finalLimit * MAX_PER_FILE, finalLimit, RERANK_TOP));
|
|
676
|
+
const displayCandidates = stage2Candidates.slice(0, displayWindow);
|
|
673
677
|
const rerankCandidates = stage2Candidates.slice(0, RERANK_TOP);
|
|
674
678
|
// Symbol-definition promotion (1/2): membership. For a bare-symbol query,
|
|
675
679
|
// ensure the chunk(s) that actually DEFINE the symbol reach the rerank set
|
|
@@ -681,6 +685,7 @@ class Searcher {
|
|
|
681
685
|
// (2/2) below then lets them win dedup over their own method-child chunks.
|
|
682
686
|
if (symbolQuery && rerankCandidates.length > 0) {
|
|
683
687
|
const present = new Set(rerankCandidates.map((d) => d.id).filter(Boolean));
|
|
688
|
+
const displayPresent = new Set(displayCandidates.map((d) => d.id).filter(Boolean));
|
|
684
689
|
const MAX_INJECT = 5;
|
|
685
690
|
let injected = 0;
|
|
686
691
|
for (const d of topCandidates) {
|
|
@@ -691,6 +696,10 @@ class Searcher {
|
|
|
691
696
|
if (readSymbolArray(d.defined_symbols).includes(symbolQuery)) {
|
|
692
697
|
rerankCandidates.push(d);
|
|
693
698
|
present.add(d.id);
|
|
699
|
+
if (!displayPresent.has(d.id)) {
|
|
700
|
+
displayCandidates.push(d);
|
|
701
|
+
displayPresent.add(d.id);
|
|
702
|
+
}
|
|
694
703
|
injected++;
|
|
695
704
|
}
|
|
696
705
|
}
|
|
@@ -719,8 +728,9 @@ class Searcher {
|
|
|
719
728
|
}
|
|
720
729
|
}
|
|
721
730
|
}
|
|
722
|
-
const
|
|
723
|
-
|
|
731
|
+
const rerankScoreByKey = new Map();
|
|
732
|
+
if (doRerank && rerankCandidates.length > 0) {
|
|
733
|
+
const scores = yield pool.rerank({
|
|
724
734
|
query: queryMatrixRaw,
|
|
725
735
|
docs: rerankCandidates.map((doc) => {
|
|
726
736
|
var _a;
|
|
@@ -733,28 +743,25 @@ class Searcher {
|
|
|
733
743
|
});
|
|
734
744
|
}),
|
|
735
745
|
colbertDim,
|
|
736
|
-
}, signal)
|
|
737
|
-
|
|
738
|
-
var _a;
|
|
739
|
-
// If rerank is disabled, fall back to fusion ordering with structural boost
|
|
746
|
+
}, signal);
|
|
747
|
+
for (const [idx, doc] of rerankCandidates.entries()) {
|
|
740
748
|
const key = doc.id || `${doc.path}:${doc.chunk_index}`;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
});
|
|
749
|
+
rerankScoreByKey.set(key, (_o = scores === null || scores === void 0 ? void 0 : scores[idx]) !== null && _o !== void 0 ? _o : 0);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
745
752
|
// Symbol-definition promotion (2/2): score. Multiplicatively boost any
|
|
746
753
|
// candidate that defines the queried symbol so the definition chunk outranks
|
|
747
754
|
// its own method-child chunks (e.g. the `BeyondError` class chunk vs its
|
|
748
755
|
// constructor/toJSON, which otherwise score higher on the literal and evict
|
|
749
756
|
// the parent in overlap dedup). Multiplicative keeps it scale-invariant
|
|
750
757
|
// across the rerank-on (ColBERT maxsim) and rerank-off (fusion) score ranges.
|
|
751
|
-
const envDefBoost = Number.parseFloat((
|
|
758
|
+
const envDefBoost = Number.parseFloat((_p = process.env.GMAX_DEF_BOOST) !== null && _p !== void 0 ? _p : "");
|
|
752
759
|
const DEF_MATCH_BOOST = Number.isFinite(envDefBoost) && envDefBoost >= 1 ? envDefBoost : 5;
|
|
753
|
-
const scored =
|
|
760
|
+
const scored = displayCandidates.map((doc, idx) => {
|
|
754
761
|
var _a, _b;
|
|
755
|
-
const base = (_a = scores === null || scores === void 0 ? void 0 : scores[idx]) !== null && _a !== void 0 ? _a : 0;
|
|
756
762
|
const key = doc.id || `${doc.path}:${doc.chunk_index}`;
|
|
757
|
-
const fusedScore = (
|
|
763
|
+
const fusedScore = (_a = candidateScores.get(key)) !== null && _a !== void 0 ? _a : 0;
|
|
764
|
+
const base = (_b = rerankScoreByKey.get(key)) !== null && _b !== void 0 ? _b : (fusedScore || 1 / (idx + 1));
|
|
758
765
|
const blended = base + FUSED_WEIGHT * fusedScore;
|
|
759
766
|
let boosted = this.applyStructureBoost(doc, blended, searchIntent);
|
|
760
767
|
if (symbolQuery &&
|
|
@@ -782,7 +789,7 @@ class Searcher {
|
|
|
782
789
|
try {
|
|
783
790
|
const { scores: prScores, max: prMax } = yield (0, pagerank_1.loadOrComputePageRank)(this.db, pathPrefix);
|
|
784
791
|
if (prMax > 0) {
|
|
785
|
-
const envWeight = Number.parseFloat((
|
|
792
|
+
const envWeight = Number.parseFloat((_q = process.env.GMAX_PR_WEIGHT) !== null && _q !== void 0 ? _q : "");
|
|
786
793
|
const PR_WEIGHT = Number.isFinite(envWeight) && envWeight >= 0 ? envWeight : 0.05;
|
|
787
794
|
for (const item of scored) {
|
|
788
795
|
const raw = item.record
|
|
@@ -799,7 +806,7 @@ class Searcher {
|
|
|
799
806
|
defs = arr.filter((v) => typeof v === "string");
|
|
800
807
|
}
|
|
801
808
|
}
|
|
802
|
-
catch (
|
|
809
|
+
catch (_t) { }
|
|
803
810
|
}
|
|
804
811
|
const norm = (0, pagerank_1.pageRankBoostForSymbols)(defs, prScores, prMax);
|
|
805
812
|
item.score += PR_WEIGHT * norm;
|
|
@@ -817,8 +824,6 @@ class Searcher {
|
|
|
817
824
|
// Item 10: Per-file diversification
|
|
818
825
|
const seenFiles = new Map();
|
|
819
826
|
const diversified = [];
|
|
820
|
-
const envMaxPerFile = Number.parseInt((_p = process.env.GMAX_MAX_PER_FILE) !== null && _p !== void 0 ? _p : "", 10);
|
|
821
|
-
const MAX_PER_FILE = Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3;
|
|
822
827
|
for (const item of uniqueScored) {
|
|
823
828
|
const path = item.record.path || "";
|
|
824
829
|
const count = seenFiles.get(path) || 0;
|
|
@@ -68,6 +68,17 @@ function isLanceCorruptionError(err) {
|
|
|
68
68
|
const msg = err instanceof Error ? err.message : String(err);
|
|
69
69
|
return /Not found:.*\.lance(?:[^a-z]|$)/i.test(msg);
|
|
70
70
|
}
|
|
71
|
+
function isMissingTableError(err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
const tableMissing = /table.*chunks.*(?:not found|does not exist)/i.test(msg) ||
|
|
74
|
+
/(?:not found|does not exist).*table.*chunks/i.test(msg) ||
|
|
75
|
+
/no such table.*chunks/i.test(msg);
|
|
76
|
+
if (tableMissing)
|
|
77
|
+
return true;
|
|
78
|
+
if (isLanceCorruptionError(err))
|
|
79
|
+
return false;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
71
82
|
const TABLE_NAME = "chunks";
|
|
72
83
|
const MAINTENANCE_INTERVAL_MS = 5 * 60 * 1000;
|
|
73
84
|
class VectorDB {
|
|
@@ -389,21 +400,24 @@ class VectorDB {
|
|
|
389
400
|
ensureTable() {
|
|
390
401
|
return __awaiter(this, void 0, void 0, function* () {
|
|
391
402
|
const db = yield this.getDb();
|
|
403
|
+
let table;
|
|
392
404
|
try {
|
|
393
|
-
|
|
394
|
-
yield this.validateSchema(table);
|
|
395
|
-
yield this.evolveSchema(table);
|
|
396
|
-
return table;
|
|
405
|
+
table = yield db.openTable(TABLE_NAME);
|
|
397
406
|
}
|
|
398
|
-
catch (
|
|
407
|
+
catch (err) {
|
|
408
|
+
if (!isMissingTableError(err))
|
|
409
|
+
throw err;
|
|
399
410
|
(0, logger_1.log)("db", `Creating table (${this.vectorDim}d)`);
|
|
400
411
|
const schema = this.buildSchema();
|
|
401
|
-
|
|
412
|
+
table = yield db.createTable(TABLE_NAME, [this.seedRow()], {
|
|
402
413
|
schema,
|
|
403
414
|
});
|
|
404
415
|
yield table.delete('id = "seed"');
|
|
405
416
|
return table;
|
|
406
417
|
}
|
|
418
|
+
yield this.validateSchema(table);
|
|
419
|
+
yield this.evolveSchema(table);
|
|
420
|
+
return table;
|
|
407
421
|
});
|
|
408
422
|
}
|
|
409
423
|
insertBatch(records) {
|
|
@@ -47,6 +47,7 @@ exports.isDaemonRunning = isDaemonRunning;
|
|
|
47
47
|
exports.isDaemonHeartbeatFresh = isDaemonHeartbeatFresh;
|
|
48
48
|
exports.readDaemonPid = readDaemonPid;
|
|
49
49
|
exports.waitForProcessExit = waitForProcessExit;
|
|
50
|
+
exports.waitForDaemonDrain = waitForDaemonDrain;
|
|
50
51
|
exports.writeDrainingMarker = writeDrainingMarker;
|
|
51
52
|
exports.clearDrainingMarker = clearDrainingMarker;
|
|
52
53
|
exports.isDaemonDraining = isDaemonDraining;
|
|
@@ -56,6 +57,8 @@ const fs = __importStar(require("node:fs"));
|
|
|
56
57
|
const net = __importStar(require("node:net"));
|
|
57
58
|
const config_1 = require("../../config");
|
|
58
59
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
60
|
+
const DAEMON_READY_TIMEOUT_MS = 30000;
|
|
61
|
+
const DAEMON_READY_POLL_MS = 200;
|
|
59
62
|
// A live daemon refreshes daemon.lock mtime every 60s (HEARTBEAT_INTERVAL_MS).
|
|
60
63
|
// Treat mtime younger than 2.5x that as proof of life, even if a ping times
|
|
61
64
|
// out — a busy daemon with a blocked event loop can still be heartbeating.
|
|
@@ -126,6 +129,15 @@ function isDaemonRunning(opts) {
|
|
|
126
129
|
return resp.ok === true;
|
|
127
130
|
});
|
|
128
131
|
}
|
|
132
|
+
function isDaemonReady(opts) {
|
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
var _a;
|
|
135
|
+
const resp = yield sendDaemonCommand({ cmd: "ping" }, { timeoutMs: (_a = opts === null || opts === void 0 ? void 0 : opts.timeoutMs) !== null && _a !== void 0 ? _a : 2000 });
|
|
136
|
+
// Old daemons do not include `ready`; treat missing as ready so an upgraded
|
|
137
|
+
// CLI does not hang on a still-running daemon from the previous release.
|
|
138
|
+
return resp.ok === true && resp.ready !== false;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
129
141
|
/**
|
|
130
142
|
* Lock-file-based liveness probe. A running daemon refreshes daemon.lock's
|
|
131
143
|
* mtime every 60s via its heartbeat loop; a fresh mtime means the daemon is
|
|
@@ -194,6 +206,11 @@ function waitForProcessExit(pid, timeoutMs) {
|
|
|
194
206
|
// LLM/MLX teardown. Treat a draining marker as authoritative for this long; past
|
|
195
207
|
// it, assume the draining daemon wedged and let killStaleProcesses reclaim it.
|
|
196
208
|
const DRAIN_GRACE_MS = 90000;
|
|
209
|
+
function waitForDaemonDrain(pid_1) {
|
|
210
|
+
return __awaiter(this, arguments, void 0, function* (pid, timeoutMs = DRAIN_GRACE_MS) {
|
|
211
|
+
return waitForProcessExit(pid, timeoutMs);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
197
214
|
/**
|
|
198
215
|
* Record that this daemon (pid) has begun graceful shutdown, so a successor's
|
|
199
216
|
* killStaleProcesses() won't SIGKILL it mid-cleanup after it drops its
|
|
@@ -240,20 +257,23 @@ function isDaemonDraining(pid) {
|
|
|
240
257
|
}
|
|
241
258
|
}
|
|
242
259
|
/**
|
|
243
|
-
* Ensure the daemon is running — start it if needed,
|
|
260
|
+
* Ensure the daemon is running — start it if needed, then wait for resources.
|
|
244
261
|
* Returns true if daemon is ready, false if it couldn't be started.
|
|
245
262
|
*/
|
|
246
263
|
function ensureDaemonRunning() {
|
|
247
264
|
return __awaiter(this, void 0, void 0, function* () {
|
|
248
|
-
if (yield
|
|
265
|
+
if (yield isDaemonReady())
|
|
249
266
|
return true;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
if (!(yield isDaemonRunning())) {
|
|
268
|
+
const { spawnDaemon } = yield Promise.resolve().then(() => __importStar(require("./daemon-launcher")));
|
|
269
|
+
const pid = spawnDaemon();
|
|
270
|
+
if (!pid)
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
|
|
274
|
+
while (Date.now() < deadline) {
|
|
275
|
+
yield new Promise((r) => setTimeout(r, DAEMON_READY_POLL_MS));
|
|
276
|
+
if (yield isDaemonReady())
|
|
257
277
|
return true;
|
|
258
278
|
}
|
|
259
279
|
return false;
|
|
@@ -315,6 +335,12 @@ function sendStreamingCommand(cmd, onProgress, opts) {
|
|
|
315
335
|
// (DB flush, compaction). Reset the watchdog; do not surface.
|
|
316
336
|
resetTimer();
|
|
317
337
|
}
|
|
338
|
+
else if (msg.ok === false) {
|
|
339
|
+
const error = typeof msg.error === "string"
|
|
340
|
+
? msg.error
|
|
341
|
+
: "daemon command failed";
|
|
342
|
+
finish(new Error(error));
|
|
343
|
+
}
|
|
318
344
|
}
|
|
319
345
|
catch (_a) {
|
|
320
346
|
console.warn("[daemon-client] Malformed response line:", line.slice(0, 200));
|
package/dist/lib/workers/pool.js
CHANGED
|
@@ -203,6 +203,7 @@ class WorkerPool {
|
|
|
203
203
|
this.destroyed = false;
|
|
204
204
|
this.destroyPromise = null;
|
|
205
205
|
this.consecutiveRespawns = 0;
|
|
206
|
+
this.respawnLimitReached = false;
|
|
206
207
|
this.idleReapInterval = null;
|
|
207
208
|
const resolved = resolveProcessWorker();
|
|
208
209
|
this.modulePath = resolved.filename;
|
|
@@ -246,6 +247,37 @@ class WorkerPool {
|
|
|
246
247
|
if (idx !== -1)
|
|
247
248
|
this.taskQueue.splice(idx, 1);
|
|
248
249
|
}
|
|
250
|
+
hasUnassignedTasks() {
|
|
251
|
+
const hasUnassigned = (queue) => queue.some((id) => {
|
|
252
|
+
const t = this.tasks.get(id);
|
|
253
|
+
return t && !t.worker;
|
|
254
|
+
});
|
|
255
|
+
return hasUnassigned(this.priorityQueue) || hasUnassigned(this.taskQueue);
|
|
256
|
+
}
|
|
257
|
+
rejectUnassignedTasks(message) {
|
|
258
|
+
for (const task of Array.from(this.tasks.values())) {
|
|
259
|
+
if (task.worker)
|
|
260
|
+
continue;
|
|
261
|
+
task.reject(new Error(message));
|
|
262
|
+
this.completeTask(task, null);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
maybeRespawn(reason, hasPendingTasks) {
|
|
266
|
+
if (this.destroyed || this.respawnLimitReached)
|
|
267
|
+
return false;
|
|
268
|
+
this.consecutiveRespawns++;
|
|
269
|
+
(0, logger_1.log)("pool", `respawn #${this.consecutiveRespawns} after ${reason} (workers=${this.workers.length} pending=${hasPendingTasks})`);
|
|
270
|
+
if (this.consecutiveRespawns > WorkerPool.MAX_RESPAWNS) {
|
|
271
|
+
this.respawnLimitReached = true;
|
|
272
|
+
const message = `Worker respawn limit reached (${WorkerPool.MAX_RESPAWNS}). Not spawning more workers.`;
|
|
273
|
+
console.error(`[pool] ${message}`);
|
|
274
|
+
if (this.workers.length === 0)
|
|
275
|
+
this.rejectUnassignedTasks(message);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
this.spawnWorker();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
249
281
|
completeTask(task, worker) {
|
|
250
282
|
this.clearTaskTimeout(task);
|
|
251
283
|
this.tasks.delete(task.id);
|
|
@@ -280,19 +312,9 @@ class WorkerPool {
|
|
|
280
312
|
this.workers = this.workers.filter((w) => w !== worker);
|
|
281
313
|
if (!this.destroyed) {
|
|
282
314
|
// Only respawn if we have no workers left or there are pending tasks
|
|
283
|
-
const
|
|
284
|
-
const t = this.tasks.get(id);
|
|
285
|
-
return t && !t.worker;
|
|
286
|
-
});
|
|
287
|
-
const hasPendingTasks = hasUnassigned(this.priorityQueue) || hasUnassigned(this.taskQueue);
|
|
315
|
+
const hasPendingTasks = this.hasUnassignedTasks();
|
|
288
316
|
if (this.workers.length === 0 || hasPendingTasks) {
|
|
289
|
-
this.
|
|
290
|
-
(0, logger_1.log)("pool", `respawn #${this.consecutiveRespawns} after exit (workers=${this.workers.length} pending=${hasPendingTasks})`);
|
|
291
|
-
if (this.consecutiveRespawns > WorkerPool.MAX_RESPAWNS) {
|
|
292
|
-
console.error(`[pool] Worker respawn limit reached (${WorkerPool.MAX_RESPAWNS}). Not spawning more workers.`);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
this.spawnWorker();
|
|
317
|
+
this.maybeRespawn(reason, hasPendingTasks);
|
|
296
318
|
}
|
|
297
319
|
this.dispatch();
|
|
298
320
|
}
|
|
@@ -350,6 +372,7 @@ class WorkerPool {
|
|
|
350
372
|
}
|
|
351
373
|
this.completeTask(task, worker);
|
|
352
374
|
this.consecutiveRespawns = 0;
|
|
375
|
+
this.respawnLimitReached = false;
|
|
353
376
|
this.recycleIfBloated(worker);
|
|
354
377
|
this.dispatch();
|
|
355
378
|
};
|
|
@@ -452,7 +475,7 @@ class WorkerPool {
|
|
|
452
475
|
catch (_e) { }
|
|
453
476
|
this.workers = this.workers.filter((w) => w !== worker);
|
|
454
477
|
if (!this.destroyed) {
|
|
455
|
-
this.
|
|
478
|
+
this.maybeRespawn(`timeout (${reason})`, this.hasUnassignedTasks());
|
|
456
479
|
}
|
|
457
480
|
this.dispatch();
|
|
458
481
|
}
|
|
@@ -472,6 +495,12 @@ class WorkerPool {
|
|
|
472
495
|
return;
|
|
473
496
|
// Lazy spawn: if no idle worker and below max, spawn one
|
|
474
497
|
if (!idle && this.workers.length < this.maxWorkers) {
|
|
498
|
+
if (this.respawnLimitReached) {
|
|
499
|
+
if (this.workers.length === 0) {
|
|
500
|
+
this.rejectUnassignedTasks(`Worker respawn limit reached (${WorkerPool.MAX_RESPAWNS}). Not spawning more workers.`);
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
475
504
|
this.spawnWorker();
|
|
476
505
|
idle = this.workers[this.workers.length - 1];
|
|
477
506
|
}
|
|
@@ -691,22 +720,35 @@ class WorkerPool {
|
|
|
691
720
|
this.taskQueue = [];
|
|
692
721
|
this.priorityQueue = [];
|
|
693
722
|
const killPromises = this.workers.map((w) => new Promise((resolve) => {
|
|
723
|
+
var _a, _b;
|
|
724
|
+
let settled = false;
|
|
725
|
+
let force;
|
|
726
|
+
let fallback;
|
|
727
|
+
const cleanup = () => {
|
|
728
|
+
if (settled)
|
|
729
|
+
return;
|
|
730
|
+
settled = true;
|
|
731
|
+
if (force)
|
|
732
|
+
clearTimeout(force);
|
|
733
|
+
if (fallback)
|
|
734
|
+
clearTimeout(fallback);
|
|
735
|
+
resolve();
|
|
736
|
+
};
|
|
694
737
|
w.cleanedUp = true;
|
|
695
738
|
w.child.removeAllListeners("message");
|
|
696
739
|
w.child.removeAllListeners("exit");
|
|
697
740
|
w.child.removeAllListeners("error");
|
|
698
|
-
w.child.once("exit",
|
|
699
|
-
|
|
700
|
-
const force = setTimeout(() => {
|
|
741
|
+
w.child.once("exit", cleanup);
|
|
742
|
+
force = setTimeout(() => {
|
|
701
743
|
try {
|
|
702
744
|
w.child.kill("SIGKILL");
|
|
703
745
|
}
|
|
704
746
|
catch (_a) { }
|
|
705
747
|
}, FORCE_KILL_GRACE_MS);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
748
|
+
(_a = force.unref) === null || _a === void 0 ? void 0 : _a.call(force);
|
|
749
|
+
fallback = setTimeout(cleanup, config_1.WORKER_TIMEOUT_MS);
|
|
750
|
+
(_b = fallback.unref) === null || _b === void 0 ? void 0 : _b.call(fallback);
|
|
751
|
+
w.child.kill("SIGTERM");
|
|
710
752
|
}));
|
|
711
753
|
this.destroyPromise = Promise.allSettled(killPromises).then(() => {
|
|
712
754
|
this.workers = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/reowens/grepmax.git"
|
|
12
12
|
},
|
|
13
|
-
"main": "index.js",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
14
|
"bin": {
|
|
15
15
|
"gmax": "dist/index.js"
|
|
16
16
|
},
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"postinstall": "node scripts/postinstall.js",
|
|
22
|
-
"prebuild": "mkdir -p dist",
|
|
22
|
+
"prebuild": "rm -rf dist tsconfig.tsbuildinfo && mkdir -p dist",
|
|
23
23
|
"build": "tsc",
|
|
24
24
|
"postbuild": "chmod +x dist/index.js",
|
|
25
25
|
"dev": "npx tsc && node dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"lint:fix": "biome check --write .",
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"prepublishOnly": "pnpm build",
|
|
40
|
-
"preversion": "pnpm test && pnpm typecheck",
|
|
40
|
+
"preversion": "pnpm test && pnpm typecheck && pnpm run format:check",
|
|
41
41
|
"version": "bash scripts/sync-versions.sh && git add -A",
|
|
42
42
|
"postversion": "bash scripts/postrelease.sh"
|
|
43
43
|
},
|
|
@@ -96,6 +96,11 @@
|
|
|
96
96
|
"web-tree-sitter": "^0.26.9",
|
|
97
97
|
"zod": "^4.4.3"
|
|
98
98
|
},
|
|
99
|
+
"pnpm": {
|
|
100
|
+
"overrides": {
|
|
101
|
+
"mathjs": "15.2.0"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
99
104
|
"devDependencies": {
|
|
100
105
|
"@biomejs/biome": "2.4.10",
|
|
101
106
|
"@types/node": "^26.0.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
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",
|
|
@@ -10,11 +10,5 @@
|
|
|
10
10
|
"homepage": "https://github.com/reowens/grepmax",
|
|
11
11
|
"repository": "https://github.com/reowens/grepmax",
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
|
-
"keywords": [
|
|
14
|
-
"search",
|
|
15
|
-
"semantic",
|
|
16
|
-
"grep",
|
|
17
|
-
"code-search",
|
|
18
|
-
"semantic-search"
|
|
19
|
-
]
|
|
13
|
+
"keywords": ["search", "semantic", "grep", "code-search", "semantic-search"]
|
|
20
14
|
}
|