@softerist/heuristic-mcp 3.0.15 → 3.0.16

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.
Files changed (49) hide show
  1. package/README.md +104 -104
  2. package/config.jsonc +173 -173
  3. package/features/ann-config.js +131 -0
  4. package/features/clear-cache.js +84 -0
  5. package/features/find-similar-code.js +291 -0
  6. package/features/hybrid-search.js +544 -0
  7. package/features/index-codebase.js +3268 -0
  8. package/features/lifecycle.js +1189 -0
  9. package/features/package-version.js +302 -0
  10. package/features/register.js +408 -0
  11. package/features/resources.js +156 -0
  12. package/features/set-workspace.js +265 -0
  13. package/index.js +96 -96
  14. package/lib/cache-ops.js +22 -22
  15. package/lib/cache-utils.js +565 -565
  16. package/lib/cache.js +1870 -1870
  17. package/lib/call-graph.js +396 -396
  18. package/lib/cli.js +1 -1
  19. package/lib/config.js +517 -517
  20. package/lib/constants.js +39 -39
  21. package/lib/embed-query-process.js +7 -7
  22. package/lib/embedding-process.js +7 -7
  23. package/lib/embedding-worker.js +299 -299
  24. package/lib/ignore-patterns.js +316 -316
  25. package/lib/json-worker.js +14 -14
  26. package/lib/json-writer.js +337 -337
  27. package/lib/logging.js +164 -164
  28. package/lib/memory-logger.js +13 -13
  29. package/lib/onnx-backend.js +193 -193
  30. package/lib/project-detector.js +84 -84
  31. package/lib/server-lifecycle.js +165 -165
  32. package/lib/settings-editor.js +754 -754
  33. package/lib/tokenizer.js +256 -256
  34. package/lib/utils.js +428 -428
  35. package/lib/vector-store-binary.js +627 -627
  36. package/lib/vector-store-sqlite.js +95 -95
  37. package/lib/workspace-env.js +28 -28
  38. package/mcp_config.json +9 -9
  39. package/package.json +86 -75
  40. package/scripts/clear-cache.js +20 -0
  41. package/scripts/download-model.js +43 -0
  42. package/scripts/mcp-launcher.js +49 -0
  43. package/scripts/postinstall.js +12 -0
  44. package/search-configs.js +36 -36
  45. package/.prettierrc +0 -7
  46. package/debug-pids.js +0 -30
  47. package/eslint.config.js +0 -36
  48. package/specs/plan.md +0 -23
  49. package/vitest.config.js +0 -39
@@ -24,51 +24,51 @@ console.info = (...args) => console.error('[INFO]', ...args);
24
24
  console.warn = (...args) => console.error('[WARN]', ...args);
25
25
 
26
26
  import { RESULT_BATCH_SIZE, DEFAULT_INFERENCE_BATCH_SIZE } from './constants.js';
27
- const workerId = Number.isInteger(workerData.workerId) ? workerData.workerId : null;
28
- const workerLabel = workerId === null ? '[Worker]' : `[Worker ${workerId}]`;
29
- const workerThreads = Number.isFinite(workerData.numThreads) ? workerData.numThreads : 1;
30
- const explicitGcEnabled = workerData.enableExplicitGc !== false;
31
- const failFastEmbeddingErrors = workerData.failFastEmbeddingErrors === true;
32
- const FAIL_FAST_CONSECUTIVE_ERROR_LIMIT = 8;
33
- const logInfo = (...args) => {
34
- console.info(...args);
35
- };
36
- let nativeBackendConfigured = false;
37
-
38
- function maybeRunGc() {
39
- if (!explicitGcEnabled || typeof global.gc !== 'function') return;
40
- global.gc();
41
- }
42
-
43
- function createFailFastState(scope) {
44
- if (!failFastEmbeddingErrors) return null;
45
- return { scope, consecutiveFailures: 0 };
46
- }
47
-
48
- function noteEmbeddingSuccess(failFastState) {
49
- if (!failFastState) return;
50
- failFastState.consecutiveFailures = 0;
51
- }
52
-
53
- function noteEmbeddingFailure(failFastState, err) {
54
- if (!failFastState) return;
55
- failFastState.consecutiveFailures += 1;
56
-
57
- if (failFastState.consecutiveFailures >= FAIL_FAST_CONSECUTIVE_ERROR_LIMIT) {
58
- const message =
59
- `${failFastState.scope}: fail-fast breaker tripped after ` +
60
- `${failFastState.consecutiveFailures} consecutive embedding failures (${err?.message || err})`;
61
- console.warn(`${workerLabel} ${message}`);
62
- throw new Error(message);
63
- }
64
-
65
- if (workerData.verbose) {
66
- console.warn(
67
- `${workerLabel} ${failFastState.scope}: embedding failure ` +
68
- `${failFastState.consecutiveFailures}/${FAIL_FAST_CONSECUTIVE_ERROR_LIMIT}`
69
- );
70
- }
71
- }
27
+ const workerId = Number.isInteger(workerData.workerId) ? workerData.workerId : null;
28
+ const workerLabel = workerId === null ? '[Worker]' : `[Worker ${workerId}]`;
29
+ const workerThreads = Number.isFinite(workerData.numThreads) ? workerData.numThreads : 1;
30
+ const explicitGcEnabled = workerData.enableExplicitGc !== false;
31
+ const failFastEmbeddingErrors = workerData.failFastEmbeddingErrors === true;
32
+ const FAIL_FAST_CONSECUTIVE_ERROR_LIMIT = 8;
33
+ const logInfo = (...args) => {
34
+ console.info(...args);
35
+ };
36
+ let nativeBackendConfigured = false;
37
+
38
+ function maybeRunGc() {
39
+ if (!explicitGcEnabled || typeof global.gc !== 'function') return;
40
+ global.gc();
41
+ }
42
+
43
+ function createFailFastState(scope) {
44
+ if (!failFastEmbeddingErrors) return null;
45
+ return { scope, consecutiveFailures: 0 };
46
+ }
47
+
48
+ function noteEmbeddingSuccess(failFastState) {
49
+ if (!failFastState) return;
50
+ failFastState.consecutiveFailures = 0;
51
+ }
52
+
53
+ function noteEmbeddingFailure(failFastState, err) {
54
+ if (!failFastState) return;
55
+ failFastState.consecutiveFailures += 1;
56
+
57
+ if (failFastState.consecutiveFailures >= FAIL_FAST_CONSECUTIVE_ERROR_LIMIT) {
58
+ const message =
59
+ `${failFastState.scope}: fail-fast breaker tripped after ` +
60
+ `${failFastState.consecutiveFailures} consecutive embedding failures (${err?.message || err})`;
61
+ console.warn(`${workerLabel} ${message}`);
62
+ throw new Error(message);
63
+ }
64
+
65
+ if (workerData.verbose) {
66
+ console.warn(
67
+ `${workerLabel} ${failFastState.scope}: embedding failure ` +
68
+ `${failFastState.consecutiveFailures}/${FAIL_FAST_CONSECUTIVE_ERROR_LIMIT}`
69
+ );
70
+ }
71
+ }
72
72
 
73
73
  function ensureNativeBackend() {
74
74
  if (nativeBackendConfigured) return;
@@ -172,7 +172,7 @@ const embeddingDimension = workerData.embeddingDimension || null;
172
172
  // Use a promise to handle concurrent calls to initializeEmbedder safely
173
173
  let embedderPromise = null;
174
174
 
175
- async function initializeEmbedder() {
175
+ async function initializeEmbedder() {
176
176
  if (!embedderPromise) {
177
177
  const modelLoadStart = Date.now();
178
178
 
@@ -183,16 +183,16 @@ async function initializeEmbedder() {
183
183
 
184
184
  embedderPromise = (async () => {
185
185
  try {
186
- ensureNativeBackend();
187
- const model = await pipeline('feature-extraction', workerData.embeddingModel, {
188
- quantized: true,
189
- dtype: 'fp32',
190
- session_options: {
191
- numThreads: workerThreads,
192
- intraOpNumThreads: workerThreads,
193
- interOpNumThreads: 1,
194
- },
195
- });
186
+ ensureNativeBackend();
187
+ const model = await pipeline('feature-extraction', workerData.embeddingModel, {
188
+ quantized: true,
189
+ dtype: 'fp32',
190
+ session_options: {
191
+ numThreads: workerThreads,
192
+ intraOpNumThreads: workerThreads,
193
+ interOpNumThreads: 1,
194
+ },
195
+ });
196
196
  const loadSeconds = ((Date.now() - modelLoadStart) / 1000).toFixed(1);
197
197
  logInfo(
198
198
  `${workerLabel} Embedding model ready: ${workerData.embeddingModel} (${loadSeconds}s)`
@@ -204,48 +204,48 @@ async function initializeEmbedder() {
204
204
  }
205
205
  })();
206
206
  }
207
- return embedderPromise;
208
- }
209
-
210
- function isFatalRuntimeEmbeddingError(err) {
211
- const message = String(err?.message || err || '').toLowerCase();
212
- return (
213
- message.includes('exception is pending') ||
214
- message.includes('invalid embedding output') ||
215
- message.includes("cannot read properties of undefined (reading 'data')") ||
216
- message.includes("cannot read properties of null (reading 'data')")
217
- );
218
- }
219
-
220
- function getEmbeddingTensor(output, { requireDimsForBatch = false, batchSize = null } = {}) {
221
- const data = output?.data;
222
- if (!data || typeof data.length !== 'number') {
223
- throw new Error('Invalid embedding output: missing tensor data');
224
- }
225
- if (!requireDimsForBatch) {
226
- return { data };
227
- }
228
-
229
- const dims = Array.isArray(output?.dims) ? output.dims : null;
230
- const hiddenSize = Number.isInteger(dims?.[dims.length - 1]) ? dims[dims.length - 1] : null;
231
- if (!hiddenSize || hiddenSize <= 0) {
232
- throw new Error('Invalid embedding output: missing tensor dims');
233
- }
234
- if (Number.isInteger(batchSize) && batchSize > 0 && data.length < hiddenSize * batchSize) {
235
- throw new Error('Invalid embedding output: tensor length mismatch');
236
- }
237
- return { data, hiddenSize };
238
- }
239
-
240
- /**
241
- * Legacy Protocol: Process chunks with optimized single-text embedding
242
- * Streams results in batches.
243
- */
244
- async function processChunks(chunks, batchId) {
245
- const embedder = await initializeEmbedder();
246
- let results = [];
247
- let transferList = [];
248
- const failFastState = createFailFastState('legacy chunk embedding');
207
+ return embedderPromise;
208
+ }
209
+
210
+ function isFatalRuntimeEmbeddingError(err) {
211
+ const message = String(err?.message || err || '').toLowerCase();
212
+ return (
213
+ message.includes('exception is pending') ||
214
+ message.includes('invalid embedding output') ||
215
+ message.includes("cannot read properties of undefined (reading 'data')") ||
216
+ message.includes("cannot read properties of null (reading 'data')")
217
+ );
218
+ }
219
+
220
+ function getEmbeddingTensor(output, { requireDimsForBatch = false, batchSize = null } = {}) {
221
+ const data = output?.data;
222
+ if (!data || typeof data.length !== 'number') {
223
+ throw new Error('Invalid embedding output: missing tensor data');
224
+ }
225
+ if (!requireDimsForBatch) {
226
+ return { data };
227
+ }
228
+
229
+ const dims = Array.isArray(output?.dims) ? output.dims : null;
230
+ const hiddenSize = Number.isInteger(dims?.[dims.length - 1]) ? dims[dims.length - 1] : null;
231
+ if (!hiddenSize || hiddenSize <= 0) {
232
+ throw new Error('Invalid embedding output: missing tensor dims');
233
+ }
234
+ if (Number.isInteger(batchSize) && batchSize > 0 && data.length < hiddenSize * batchSize) {
235
+ throw new Error('Invalid embedding output: tensor length mismatch');
236
+ }
237
+ return { data, hiddenSize };
238
+ }
239
+
240
+ /**
241
+ * Legacy Protocol: Process chunks with optimized single-text embedding
242
+ * Streams results in batches.
243
+ */
244
+ async function processChunks(chunks, batchId) {
245
+ const embedder = await initializeEmbedder();
246
+ let results = [];
247
+ let transferList = [];
248
+ const failFastState = createFailFastState('legacy chunk embedding');
249
249
 
250
250
  const flush = (done = false) => {
251
251
  // Only flush intermediate results when we have enough for a batch
@@ -270,16 +270,16 @@ async function processChunks(chunks, batchId) {
270
270
  };
271
271
 
272
272
  for (const chunk of chunks) {
273
- try {
274
- const output = await embedder(chunk.text, {
275
- pooling: 'mean',
276
- normalize: true,
277
- });
278
- // CRITICAL: Deep copy to release ONNX tensor memory
279
- const { data } = getEmbeddingTensor(output);
280
- let vector = new Float32Array(data);
281
- // Apply MRL dimension slicing if configured
282
- vector = sliceAndNormalize(vector, embeddingDimension);
273
+ try {
274
+ const output = await embedder(chunk.text, {
275
+ pooling: 'mean',
276
+ normalize: true,
277
+ });
278
+ // CRITICAL: Deep copy to release ONNX tensor memory
279
+ const { data } = getEmbeddingTensor(output);
280
+ let vector = new Float32Array(data);
281
+ // Apply MRL dimension slicing if configured
282
+ vector = sliceAndNormalize(vector, embeddingDimension);
283
283
  // Properly dispose tensor to release ONNX runtime memory
284
284
  if (typeof output.dispose === 'function')
285
285
  try {
@@ -289,37 +289,37 @@ async function processChunks(chunks, batchId) {
289
289
  console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
290
290
  }
291
291
  }
292
- results.push({
293
- file: chunk.file,
294
- startLine: chunk.startLine,
295
- endLine: chunk.endLine,
296
- content: chunk.text,
297
- vector,
298
- success: true,
299
- });
300
- transferList.push(vector.buffer);
301
- noteEmbeddingSuccess(failFastState);
302
- } catch (error) {
303
- results.push({
304
- file: chunk.file,
305
- startLine: chunk.startLine,
306
- endLine: chunk.endLine,
307
- error: error.message,
308
- success: false,
309
- });
310
- noteEmbeddingFailure(failFastState, error);
311
- if (isFatalRuntimeEmbeddingError(error)) {
312
- throw error;
313
- }
314
- }
315
- flush();
316
- }
317
-
318
- flush(true);
319
-
320
- // Force GC if available to free massive tensor buffers immediately
321
- maybeRunGc();
322
- }
292
+ results.push({
293
+ file: chunk.file,
294
+ startLine: chunk.startLine,
295
+ endLine: chunk.endLine,
296
+ content: chunk.text,
297
+ vector,
298
+ success: true,
299
+ });
300
+ transferList.push(vector.buffer);
301
+ noteEmbeddingSuccess(failFastState);
302
+ } catch (error) {
303
+ results.push({
304
+ file: chunk.file,
305
+ startLine: chunk.startLine,
306
+ endLine: chunk.endLine,
307
+ error: error.message,
308
+ success: false,
309
+ });
310
+ noteEmbeddingFailure(failFastState, error);
311
+ if (isFatalRuntimeEmbeddingError(error)) {
312
+ throw error;
313
+ }
314
+ }
315
+ flush();
316
+ }
317
+
318
+ flush(true);
319
+
320
+ // Force GC if available to free massive tensor buffers immediately
321
+ maybeRunGc();
322
+ }
323
323
 
324
324
  // =====================================================================
325
325
  // SHARED HELPER FUNCTIONS
@@ -434,9 +434,9 @@ function processFileMetadata(file, content, options) {
434
434
  * New Protocol: Process entire file (read, chunk, embed) in worker.
435
435
  * Returns results once processing is complete.
436
436
  */
437
- async function processFileTask(message) {
438
- const embedder = await initializeEmbedder();
439
- const failFastState = createFailFastState(`file-task ${path.basename(message.file || '')}`);
437
+ async function processFileTask(message) {
438
+ const embedder = await initializeEmbedder();
439
+ const failFastState = createFailFastState(`file-task ${path.basename(message.file || '')}`);
440
440
 
441
441
  const file = message.file;
442
442
  const force = !!message.force;
@@ -488,38 +488,38 @@ async function processFileTask(message) {
488
488
 
489
489
  // Batch size for inference (balance between speed and memory)
490
490
  // Configurable via workerData, default 4 balances memory and throughput
491
- const INFERENCE_BATCH_SIZE = Number.isInteger(workerData.inferenceBatchSize)
492
- ? workerData.inferenceBatchSize
493
- : DEFAULT_INFERENCE_BATCH_SIZE;
494
- let processedSinceGc = 0;
495
-
496
- for (let i = 0; i < chunks.length; i += INFERENCE_BATCH_SIZE) {
491
+ const INFERENCE_BATCH_SIZE = Number.isInteger(workerData.inferenceBatchSize)
492
+ ? workerData.inferenceBatchSize
493
+ : DEFAULT_INFERENCE_BATCH_SIZE;
494
+ let processedSinceGc = 0;
495
+
496
+ for (let i = 0; i < chunks.length; i += INFERENCE_BATCH_SIZE) {
497
497
  const batchChunks = chunks.slice(i, i + INFERENCE_BATCH_SIZE);
498
498
  const batchTexts = batchChunks.map((c) => c.text);
499
499
 
500
- try {
501
- // Run inference on the batch
502
- const output = await embedder(batchTexts, {
503
- pooling: 'mean',
504
- normalize: true,
505
- });
506
-
507
- // Output is a Tensor with shape [batch_size, hidden_size]
508
- // data is a flat Float32Array
509
- const { data, hiddenSize } = getEmbeddingTensor(output, {
510
- requireDimsForBatch: true,
511
- batchSize: batchChunks.length,
512
- });
513
-
514
- for (let j = 0; j < batchChunks.length; j++) {
515
- const c = batchChunks[j];
516
-
517
- // Slice the flat buffer to get this chunk's vector
518
- // specific slice for this element
519
- const start = j * hiddenSize;
520
- const end = start + hiddenSize;
521
- const vectorView =
522
- typeof data.subarray === 'function' ? data.subarray(start, end) : data.slice(start, end);
500
+ try {
501
+ // Run inference on the batch
502
+ const output = await embedder(batchTexts, {
503
+ pooling: 'mean',
504
+ normalize: true,
505
+ });
506
+
507
+ // Output is a Tensor with shape [batch_size, hidden_size]
508
+ // data is a flat Float32Array
509
+ const { data, hiddenSize } = getEmbeddingTensor(output, {
510
+ requireDimsForBatch: true,
511
+ batchSize: batchChunks.length,
512
+ });
513
+
514
+ for (let j = 0; j < batchChunks.length; j++) {
515
+ const c = batchChunks[j];
516
+
517
+ // Slice the flat buffer to get this chunk's vector
518
+ // specific slice for this element
519
+ const start = j * hiddenSize;
520
+ const end = start + hiddenSize;
521
+ const vectorView =
522
+ typeof data.subarray === 'function' ? data.subarray(start, end) : data.slice(start, end);
523
523
 
524
524
  // Deep copy to ensure independent buffer for transfer
525
525
  let vector = new Float32Array(vectorView);
@@ -535,29 +535,29 @@ async function processFileTask(message) {
535
535
  transferList.push(vector.buffer);
536
536
  }
537
537
  // Properly dispose tensor to release ONNX runtime memory
538
- if (typeof output.dispose === 'function')
539
- try {
540
- output.dispose();
541
- } catch (disposeErr) {
542
- if (workerData.verbose) {
543
- console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
544
- }
545
- }
546
- noteEmbeddingSuccess(failFastState);
547
- } catch (err) {
548
- if (isFatalRuntimeEmbeddingError(err)) {
549
- noteEmbeddingFailure(failFastState, err);
550
- throw err;
551
- }
552
- // Fallback: if batch fails (e.g. OOM), try one by one for this batch
553
- console.warn(`${workerLabel} Batch inference failed (${err.name}), retrying individually: ${err.message}`);
554
- noteEmbeddingFailure(failFastState, err);
555
-
556
- for (const c of batchChunks) {
557
- try {
558
- const output = await embedder(c.text, { pooling: 'mean', normalize: true });
559
- const { data } = getEmbeddingTensor(output);
560
- let vector = new Float32Array(data);
538
+ if (typeof output.dispose === 'function')
539
+ try {
540
+ output.dispose();
541
+ } catch (disposeErr) {
542
+ if (workerData.verbose) {
543
+ console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
544
+ }
545
+ }
546
+ noteEmbeddingSuccess(failFastState);
547
+ } catch (err) {
548
+ if (isFatalRuntimeEmbeddingError(err)) {
549
+ noteEmbeddingFailure(failFastState, err);
550
+ throw err;
551
+ }
552
+ // Fallback: if batch fails (e.g. OOM), try one by one for this batch
553
+ console.warn(`${workerLabel} Batch inference failed (${err.name}), retrying individually: ${err.message}`);
554
+ noteEmbeddingFailure(failFastState, err);
555
+
556
+ for (const c of batchChunks) {
557
+ try {
558
+ const output = await embedder(c.text, { pooling: 'mean', normalize: true });
559
+ const { data } = getEmbeddingTensor(output);
560
+ let vector = new Float32Array(data);
561
561
  // Apply MRL dimension slicing if configured
562
562
  vector = sliceAndNormalize(vector, embeddingDimension);
563
563
  // Properly dispose tensor to release ONNX runtime memory
@@ -569,36 +569,36 @@ async function processFileTask(message) {
569
569
  console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
570
570
  }
571
571
  }
572
- results.push({
573
- startLine: c.startLine,
574
- endLine: c.endLine,
575
- text: c.text,
576
- vectorBuffer: vector.buffer,
577
- });
578
- transferList.push(vector.buffer);
579
- noteEmbeddingSuccess(failFastState);
580
- } catch (innerErr) {
581
- // Note: No tensor disposal needed - embedder() threw before returning a tensor
582
- console.warn(`${workerLabel} Chunk embedding failed: ${innerErr.message}`);
583
- // We omit this chunk from results, effectively skipping it
584
- noteEmbeddingFailure(failFastState, innerErr);
585
- if (isFatalRuntimeEmbeddingError(innerErr)) {
586
- throw innerErr;
587
- }
588
- }
589
- }
590
- }
591
-
592
- // Yield to event loop briefly between batches and trigger GC
593
- processedSinceGc += batchChunks.length;
594
- if (chunks.length > INFERENCE_BATCH_SIZE) {
595
- if (processedSinceGc >= 100) {
596
- maybeRunGc();
597
- processedSinceGc = 0;
598
- }
599
- await new Promise((resolve) => setTimeout(resolve, 0));
600
- }
601
- }
572
+ results.push({
573
+ startLine: c.startLine,
574
+ endLine: c.endLine,
575
+ text: c.text,
576
+ vectorBuffer: vector.buffer,
577
+ });
578
+ transferList.push(vector.buffer);
579
+ noteEmbeddingSuccess(failFastState);
580
+ } catch (innerErr) {
581
+ // Note: No tensor disposal needed - embedder() threw before returning a tensor
582
+ console.warn(`${workerLabel} Chunk embedding failed: ${innerErr.message}`);
583
+ // We omit this chunk from results, effectively skipping it
584
+ noteEmbeddingFailure(failFastState, innerErr);
585
+ if (isFatalRuntimeEmbeddingError(innerErr)) {
586
+ throw innerErr;
587
+ }
588
+ }
589
+ }
590
+ }
591
+
592
+ // Yield to event loop briefly between batches and trigger GC
593
+ processedSinceGc += batchChunks.length;
594
+ if (chunks.length > INFERENCE_BATCH_SIZE) {
595
+ if (processedSinceGc >= 100) {
596
+ maybeRunGc();
597
+ processedSinceGc = 0;
598
+ }
599
+ await new Promise((resolve) => setTimeout(resolve, 0));
600
+ }
601
+ }
602
602
 
603
603
  return { status: 'indexed', hash, mtimeMs, size, callData, results, transferList };
604
604
  }
@@ -635,13 +635,13 @@ parentPort.on('message', async (message) => {
635
635
 
636
636
  // Clear references
637
637
  embedderPromise = null;
638
- }
639
-
640
- // Trigger garbage collection if available
641
- if (explicitGcEnabled && typeof global.gc === 'function') {
642
- const before = process.memoryUsage();
643
- global.gc();
644
- const after = process.memoryUsage();
638
+ }
639
+
640
+ // Trigger garbage collection if available
641
+ if (explicitGcEnabled && typeof global.gc === 'function') {
642
+ const before = process.memoryUsage();
643
+ global.gc();
644
+ const after = process.memoryUsage();
645
645
  logInfo(
646
646
  `${workerLabel} Post-unload GC: rss ${(before.rss / 1024 / 1024).toFixed(1)}MB -> ${(after.rss / 1024 / 1024).toFixed(1)}MB`
647
647
  );
@@ -672,10 +672,10 @@ parentPort.on('message', async (message) => {
672
672
  }
673
673
 
674
674
  // ---- Batch file processing ----
675
- if (message.type === 'processFiles') {
676
- const { files, batchId } = message;
677
- const batchTransfer = [];
678
- const failFastState = createFailFastState('cross-file batch embedding');
675
+ if (message.type === 'processFiles') {
676
+ const { files, batchId } = message;
677
+ const batchTransfer = [];
678
+ const failFastState = createFailFastState('cross-file batch embedding');
679
679
 
680
680
  // 1. Pre-process all files: Read, Stat, and Chunk
681
681
  // We do this first to gather a massive list of chunks for batched inference
@@ -760,13 +760,13 @@ parentPort.on('message', async (message) => {
760
760
  continue;
761
761
  }
762
762
 
763
- const { hash, callData, chunks } = meta;
764
- const chunkCount = chunks.length;
765
-
766
- // Trigger GC every 100 files
767
- if ((i + 1) % 100 === 0) {
768
- maybeRunGc();
769
- }
763
+ const { hash, callData, chunks } = meta;
764
+ const chunkCount = chunks.length;
765
+
766
+ // Trigger GC every 100 files
767
+ if ((i + 1) % 100 === 0) {
768
+ maybeRunGc();
769
+ }
770
770
 
771
771
  // Register chunks for batching
772
772
  if (chunks.length > 0) {
@@ -814,52 +814,52 @@ parentPort.on('message', async (message) => {
814
814
  const batchSlice = allPendingChunks.slice(i, i + INFERENCE_BATCH_SIZE);
815
815
  const batchTexts = batchSlice.map((c) => c.text);
816
816
 
817
- try {
818
- const output = await embedder(batchTexts, { pooling: 'mean', normalize: true });
819
- const { data, hiddenSize } = getEmbeddingTensor(output, {
820
- requireDimsForBatch: true,
821
- batchSize: batchSlice.length,
822
- });
823
-
824
- for (let j = 0; j < batchSlice.length; j++) {
825
- const start = j * hiddenSize;
826
- const end = start + hiddenSize;
827
- const vectorView =
828
- typeof data.subarray === 'function'
829
- ? data.subarray(start, end)
830
- : data.slice(start, end);
831
- // Deep copy the view to avoid WASM memory issues, then apply MRL slicing
832
- const vector = sliceAndNormalize(new Float32Array(vectorView), embeddingDimension);
817
+ try {
818
+ const output = await embedder(batchTexts, { pooling: 'mean', normalize: true });
819
+ const { data, hiddenSize } = getEmbeddingTensor(output, {
820
+ requireDimsForBatch: true,
821
+ batchSize: batchSlice.length,
822
+ });
823
+
824
+ for (let j = 0; j < batchSlice.length; j++) {
825
+ const start = j * hiddenSize;
826
+ const end = start + hiddenSize;
827
+ const vectorView =
828
+ typeof data.subarray === 'function'
829
+ ? data.subarray(start, end)
830
+ : data.slice(start, end);
831
+ // Deep copy the view to avoid WASM memory issues, then apply MRL slicing
832
+ const vector = sliceAndNormalize(new Float32Array(vectorView), embeddingDimension);
833
833
 
834
834
  batchSlice[j].vectorBuffer = vector.buffer;
835
835
  batchTransfer.push(vector.buffer);
836
836
  }
837
837
  // Properly dispose tensor to release ONNX runtime memory
838
- if (typeof output.dispose === 'function')
839
- try {
840
- output.dispose();
841
- } catch (disposeErr) {
842
- if (workerData.verbose) {
843
- console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
844
- }
845
- }
846
- noteEmbeddingSuccess(failFastState);
847
- } catch (err) {
848
- if (isFatalRuntimeEmbeddingError(err)) {
849
- noteEmbeddingFailure(failFastState, err);
850
- throw err;
851
- }
852
- console.warn(
853
- `${workerLabel} Cross-file batch inference failed, retrying individually: ${err.message}`
854
- );
855
- noteEmbeddingFailure(failFastState, err);
856
- // Fallback: individual embedding for this failed batch
857
- for (const item of batchSlice) {
858
- try {
859
- const output = await embedder(item.text, { pooling: 'mean', normalize: true });
860
- const { data } = getEmbeddingTensor(output);
861
- // Deep copy and apply MRL slicing
862
- const vector = sliceAndNormalize(new Float32Array(data), embeddingDimension);
838
+ if (typeof output.dispose === 'function')
839
+ try {
840
+ output.dispose();
841
+ } catch (disposeErr) {
842
+ if (workerData.verbose) {
843
+ console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
844
+ }
845
+ }
846
+ noteEmbeddingSuccess(failFastState);
847
+ } catch (err) {
848
+ if (isFatalRuntimeEmbeddingError(err)) {
849
+ noteEmbeddingFailure(failFastState, err);
850
+ throw err;
851
+ }
852
+ console.warn(
853
+ `${workerLabel} Cross-file batch inference failed, retrying individually: ${err.message}`
854
+ );
855
+ noteEmbeddingFailure(failFastState, err);
856
+ // Fallback: individual embedding for this failed batch
857
+ for (const item of batchSlice) {
858
+ try {
859
+ const output = await embedder(item.text, { pooling: 'mean', normalize: true });
860
+ const { data } = getEmbeddingTensor(output);
861
+ // Deep copy and apply MRL slicing
862
+ const vector = sliceAndNormalize(new Float32Array(data), embeddingDimension);
863
863
  // Properly dispose tensor to release ONNX runtime memory
864
864
  if (typeof output.dispose === 'function')
865
865
  try {
@@ -869,18 +869,18 @@ parentPort.on('message', async (message) => {
869
869
  console.warn(`${workerLabel} Failed to dispose tensor: ${disposeErr.message}`);
870
870
  }
871
871
  }
872
- item.vectorBuffer = vector.buffer;
873
- batchTransfer.push(vector.buffer);
874
- noteEmbeddingSuccess(failFastState);
875
- } catch (innerErr) {
876
- console.warn(`${workerLabel} Chunk embedding failed: ${innerErr.message}`);
877
- noteEmbeddingFailure(failFastState, innerErr);
878
- if (isFatalRuntimeEmbeddingError(innerErr)) {
879
- throw innerErr;
880
- }
881
- }
882
- }
883
- }
872
+ item.vectorBuffer = vector.buffer;
873
+ batchTransfer.push(vector.buffer);
874
+ noteEmbeddingSuccess(failFastState);
875
+ } catch (innerErr) {
876
+ console.warn(`${workerLabel} Chunk embedding failed: ${innerErr.message}`);
877
+ noteEmbeddingFailure(failFastState, innerErr);
878
+ if (isFatalRuntimeEmbeddingError(innerErr)) {
879
+ throw innerErr;
880
+ }
881
+ }
882
+ }
883
+ }
884
884
 
885
885
  // Minimal yield to keep event loop breathing (optional, can be removed for max throughput)
886
886
  if (allPendingChunks.length > 50 && i % 50 === 0) {
@@ -944,10 +944,10 @@ parentPort.on('message', async (message) => {
944
944
  batchTransfer
945
945
  );
946
946
 
947
- // Explicitly clear references and trigger GC
948
- batchTransfer.length = 0;
949
- maybeRunGc();
950
- return;
947
+ // Explicitly clear references and trigger GC
948
+ batchTransfer.length = 0;
949
+ maybeRunGc();
950
+ return;
951
951
  }
952
952
 
953
953
  // ---- Legacy protocol: batch of chunks prepared by main thread ----