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.
@@ -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(() => this.processBatch(), DEBOUNCE_MS);
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(() => this.processBatch(), 60000);
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 attempted = new Set();
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 weren't attempted (aborted or pool unhealthy)
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 (!attempted.has(absPath) && !this.pending.has(absPath)) {
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(() => this.processBatch(), backoffOverrideMs);
379
+ this.debounceTimer = setTimeout(() => {
380
+ this.debounceTimer = null;
381
+ this.startBatch();
382
+ }, backoffOverrideMs);
347
383
  }
348
384
  else {
349
385
  this.scheduleBatch();
@@ -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 (_q) {
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 (_r) { }
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 scores = doRerank
723
- ? yield pool.rerank({
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
- : rerankCandidates.map((doc, idx) => {
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
- const fusedScore = (_a = candidateScores.get(key)) !== null && _a !== void 0 ? _a : 0;
742
- // Small tie-breaker so later items don't all share 0
743
- return fusedScore || 1 / (idx + 1);
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((_m = process.env.GMAX_DEF_BOOST) !== null && _m !== void 0 ? _m : "");
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 = rerankCandidates.map((doc, idx) => {
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 = (_b = candidateScores.get(key)) !== null && _b !== void 0 ? _b : 0;
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((_o = process.env.GMAX_PR_WEIGHT) !== null && _o !== void 0 ? _o : "");
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 (_s) { }
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
- const table = yield db.openTable(TABLE_NAME);
394
- yield this.validateSchema(table);
395
- yield this.evolveSchema(table);
396
- return table;
405
+ table = yield db.openTable(TABLE_NAME);
397
406
  }
398
- catch (_err) {
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
- const table = yield db.createTable(TABLE_NAME, [this.seedRow()], {
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, poll up to 5s.
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 isDaemonRunning())
265
+ if (yield isDaemonReady())
249
266
  return true;
250
- const { spawnDaemon } = yield Promise.resolve().then(() => __importStar(require("./daemon-launcher")));
251
- const pid = spawnDaemon();
252
- if (!pid)
253
- return false;
254
- for (let i = 0; i < 25; i++) {
255
- yield new Promise((r) => setTimeout(r, 200));
256
- if (yield isDaemonRunning())
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));
@@ -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 hasUnassigned = (queue) => queue.some((id) => {
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.consecutiveRespawns++;
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.spawnWorker();
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", () => resolve());
699
- w.child.kill("SIGTERM");
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
- setTimeout(() => {
707
- clearTimeout(force);
708
- resolve();
709
- }, config_1.WORKER_TIMEOUT_MS);
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.21.3",
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.21.3",
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
  }