grepmax 0.21.2 → 0.22.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.
@@ -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 {
@@ -271,6 +282,7 @@ class VectorDB {
271
282
  defined_symbols: [],
272
283
  referenced_symbols: [],
273
284
  type_referenced_symbols: [],
285
+ member_referenced_symbols: [],
274
286
  imports: [],
275
287
  exports: [],
276
288
  role: "",
@@ -340,6 +352,7 @@ class VectorDB {
340
352
  new apache_arrow_1.Field("defined_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
341
353
  new apache_arrow_1.Field("referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
342
354
  new apache_arrow_1.Field("type_referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
355
+ new apache_arrow_1.Field("member_referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
343
356
  new apache_arrow_1.Field("imports", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
344
357
  new apache_arrow_1.Field("exports", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true),
345
358
  new apache_arrow_1.Field("role", new apache_arrow_1.Utf8(), true),
@@ -360,42 +373,56 @@ class VectorDB {
360
373
  return __awaiter(this, void 0, void 0, function* () {
361
374
  const schema = yield table.schema();
362
375
  const fields = new Set(schema.fields.map((f) => f.name));
363
- if (fields.has("type_referenced_symbols"))
364
- return;
365
- try {
366
- yield table.addColumns(new apache_arrow_1.Field("type_referenced_symbols", new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true));
367
- (0, logger_1.log)("db", "Added type_referenced_symbols column to existing table");
368
- }
369
- catch (err) {
370
- // Lost a race with another writer that already added it, or a transient
371
- // commit conflict — the next ensureTable() re-checks and no-ops.
372
- (0, logger_1.debug)("vectordb", `evolveSchema skipped: ${err.message}`);
376
+ // Additive list columns that older tables may predate. Each is checked and
377
+ // added INDEPENDENTLY: a single early-return on the first existing column
378
+ // (as this used to do) would permanently strand every table that already has
379
+ // `type_referenced_symbols` i.e. all of them — without ever gaining a newer
380
+ // column like `member_referenced_symbols`.
381
+ const additiveListColumns = [
382
+ "type_referenced_symbols",
383
+ "member_referenced_symbols",
384
+ ];
385
+ for (const col of additiveListColumns) {
386
+ if (fields.has(col))
387
+ continue;
388
+ try {
389
+ yield table.addColumns(new apache_arrow_1.Field(col, new apache_arrow_1.List(new apache_arrow_1.Field("item", new apache_arrow_1.Utf8(), true)), true));
390
+ (0, logger_1.log)("db", `Added ${col} column to existing table`);
391
+ }
392
+ catch (err) {
393
+ // Lost a race with another writer that already added it, or a transient
394
+ // commit conflict — the next ensureTable() re-checks and no-ops.
395
+ (0, logger_1.debug)("vectordb", `evolveSchema ${col} skipped: ${err.message}`);
396
+ }
373
397
  }
374
398
  });
375
399
  }
376
400
  ensureTable() {
377
401
  return __awaiter(this, void 0, void 0, function* () {
378
402
  const db = yield this.getDb();
403
+ let table;
379
404
  try {
380
- const table = yield db.openTable(TABLE_NAME);
381
- yield this.validateSchema(table);
382
- yield this.evolveSchema(table);
383
- return table;
405
+ table = yield db.openTable(TABLE_NAME);
384
406
  }
385
- catch (_err) {
407
+ catch (err) {
408
+ if (!isMissingTableError(err))
409
+ throw err;
386
410
  (0, logger_1.log)("db", `Creating table (${this.vectorDim}d)`);
387
411
  const schema = this.buildSchema();
388
- const table = yield db.createTable(TABLE_NAME, [this.seedRow()], {
412
+ table = yield db.createTable(TABLE_NAME, [this.seedRow()], {
389
413
  schema,
390
414
  });
391
415
  yield table.delete('id = "seed"');
392
416
  return table;
393
417
  }
418
+ yield this.validateSchema(table);
419
+ yield this.evolveSchema(table);
420
+ return table;
394
421
  });
395
422
  }
396
423
  insertBatch(records) {
397
424
  return __awaiter(this, void 0, void 0, function* () {
398
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
425
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
399
426
  if (!records.length)
400
427
  return;
401
428
  this.ensureDiskOk();
@@ -473,12 +500,14 @@ class VectorDB {
473
500
  rec.defined_symbols = (_g = rec.defined_symbols) !== null && _g !== void 0 ? _g : [];
474
501
  rec.referenced_symbols = (_h = rec.referenced_symbols) !== null && _h !== void 0 ? _h : [];
475
502
  rec.type_referenced_symbols = (_j = rec.type_referenced_symbols) !== null && _j !== void 0 ? _j : [];
476
- rec.imports = (_k = rec.imports) !== null && _k !== void 0 ? _k : [];
477
- rec.exports = (_l = rec.exports) !== null && _l !== void 0 ? _l : [];
478
- rec.role = (_m = rec.role) !== null && _m !== void 0 ? _m : "";
479
- rec.parent_symbol = (_o = rec.parent_symbol) !== null && _o !== void 0 ? _o : "";
480
- rec.file_skeleton = (_p = rec.file_skeleton) !== null && _p !== void 0 ? _p : "";
481
- rec.summary = (_q = rec.summary) !== null && _q !== void 0 ? _q : null;
503
+ rec.member_referenced_symbols =
504
+ (_k = rec.member_referenced_symbols) !== null && _k !== void 0 ? _k : [];
505
+ rec.imports = (_l = rec.imports) !== null && _l !== void 0 ? _l : [];
506
+ rec.exports = (_m = rec.exports) !== null && _m !== void 0 ? _m : [];
507
+ rec.role = (_o = rec.role) !== null && _o !== void 0 ? _o : "";
508
+ rec.parent_symbol = (_p = rec.parent_symbol) !== null && _p !== void 0 ? _p : "";
509
+ rec.file_skeleton = (_q = rec.file_skeleton) !== null && _q !== void 0 ? _q : "";
510
+ rec.summary = (_r = rec.summary) !== null && _r !== void 0 ? _r : null;
482
511
  }
483
512
  try {
484
513
  yield this.withWriteGate(() => table.add(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));
@@ -227,6 +227,7 @@ class WorkerOrchestrator {
227
227
  defined_symbols: chunk.definedSymbols,
228
228
  referenced_symbols: chunk.referencedSymbols,
229
229
  type_referenced_symbols: chunk.typeReferencedSymbols,
230
+ member_referenced_symbols: chunk.memberReferencedSymbols,
230
231
  role: chunk.role,
231
232
  parent_symbol: chunk.parentSymbol,
232
233
  file_skeleton: chunk.isAnchor ? skeleton : undefined,
@@ -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.2",
3
+ "version": "0.22.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",
@@ -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.2",
3
+ "version": "0.22.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
  }