@softerist/heuristic-mcp 3.2.3 → 3.2.5

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 (46) hide show
  1. package/README.md +387 -376
  2. package/config.jsonc +800 -800
  3. package/features/ann-config.js +102 -110
  4. package/features/clear-cache.js +81 -84
  5. package/features/find-similar-code.js +265 -286
  6. package/features/hybrid-search.js +487 -536
  7. package/features/index-codebase.js +3146 -3271
  8. package/features/lifecycle.js +1011 -1063
  9. package/features/package-version.js +277 -291
  10. package/features/register.js +351 -370
  11. package/features/resources.js +115 -130
  12. package/features/set-workspace.js +214 -240
  13. package/index.js +788 -781
  14. package/lib/cache-ops.js +22 -22
  15. package/lib/cache-utils.js +465 -519
  16. package/lib/cache.js +1749 -1849
  17. package/lib/call-graph.js +396 -396
  18. package/lib/cli.js +232 -226
  19. package/lib/config.js +1483 -1495
  20. package/lib/constants.js +511 -493
  21. package/lib/embed-query-process.js +206 -212
  22. package/lib/embedding-process.js +434 -451
  23. package/lib/embedding-worker.js +862 -934
  24. package/lib/ignore-patterns.js +276 -316
  25. package/lib/json-worker.js +14 -14
  26. package/lib/json-writer.js +302 -310
  27. package/lib/logging.js +133 -127
  28. package/lib/memory-logger.js +13 -13
  29. package/lib/onnx-backend.js +188 -193
  30. package/lib/path-utils.js +18 -23
  31. package/lib/project-detector.js +82 -84
  32. package/lib/server-lifecycle.js +164 -147
  33. package/lib/settings-editor.js +738 -739
  34. package/lib/slice-normalize.js +25 -31
  35. package/lib/tokenizer.js +168 -203
  36. package/lib/utils.js +364 -409
  37. package/lib/vector-store-binary.js +973 -991
  38. package/lib/vector-store-sqlite.js +377 -414
  39. package/lib/workspace-env.js +32 -34
  40. package/mcp_config.json +9 -9
  41. package/package.json +86 -86
  42. package/scripts/clear-cache.js +20 -20
  43. package/scripts/download-model.js +43 -43
  44. package/scripts/mcp-launcher.js +49 -49
  45. package/scripts/postinstall.js +12 -12
  46. package/search-configs.js +36 -36
@@ -1,451 +1,434 @@
1
- import { pipeline, env } from '@huggingface/transformers';
2
- import { configureNativeOnnxBackend } from './onnx-backend.js';
3
- import {
4
- EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
5
- EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
6
- EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
7
- EMBEDDING_PROCESS_GC_STATE_INITIAL,
8
- } from './constants.js';
9
- import readline from 'readline';
10
- import path from 'path';
11
- import os from 'os';
12
- import { pathToFileURL } from 'url';
13
-
14
-
15
- let currentRequestId = -1;
16
- const log = (...args) => {
17
- if (currentRequestId > 0 && !process.env.EMBEDDING_PROCESS_VERBOSE) {
18
- return;
19
- }
20
- console.error(...args);
21
- };
22
-
23
- function formatMemory() {
24
- const usage = process.memoryUsage();
25
- return `rss=${(usage.rss / 1024 / 1024).toFixed(1)}MB heap=${(usage.heapUsed / 1024 / 1024).toFixed(1)}MB`;
26
- }
27
-
28
- function readStdin() {
29
- return new Promise((resolve, reject) => {
30
- let data = '';
31
- process.stdin.setEncoding('utf8');
32
- process.stdin.on('data', (chunk) => {
33
- data += chunk;
34
- });
35
- process.stdin.on('end', () => resolve(data));
36
- process.stdin.on('error', reject);
37
- });
38
- }
39
-
40
- const persistent = process.env.EMBEDDING_PROCESS_PERSISTENT === 'true';
41
- let embedderPromise = null;
42
- let configuredThreads = null;
43
- let configuredModel = null;
44
- let requestCounter = 0;
45
- let gcSupported = typeof global.gc === 'function';
46
- let nativeBackendConfigured = false;
47
- const gcState = { ...EMBEDDING_PROCESS_GC_STATE_INITIAL };
48
-
49
- function getGlobalCacheDir() {
50
- if (process.platform === 'win32') {
51
- return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
52
- }
53
- if (process.platform === 'darwin') {
54
- return path.join(os.homedir(), 'Library', 'Caches');
55
- }
56
- return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
57
- }
58
-
59
- function toPositiveNumber(value, fallback) {
60
- const parsed = Number(value);
61
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
62
- }
63
-
64
- function toNonNegativeInteger(value, fallback) {
65
- const parsed = Number.parseInt(value, 10);
66
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
67
- }
68
-
69
- function toPositiveInteger(value, fallback) {
70
- const parsed = Number.parseInt(value, 10);
71
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
72
- }
73
-
74
- function resolveGcPolicy(payload) {
75
- return {
76
- rssThresholdMb: toPositiveNumber(
77
- payload?.gcRssThresholdMb,
78
- EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB
79
- ),
80
- minIntervalMs: toNonNegativeInteger(
81
- payload?.gcMinIntervalMs,
82
- EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS
83
- ),
84
- maxRequestsWithoutCollection: toPositiveInteger(
85
- payload?.gcMaxRequestsWithoutCollection,
86
- EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION
87
- ),
88
- };
89
- }
90
-
91
- function maybeRunGc(policy, { reason = 'unknown', force = false } = {}) {
92
- if (!gcSupported) return false;
93
-
94
- const before = process.memoryUsage();
95
- const rssBeforeMb = before.rss / 1024 / 1024;
96
- const rssTrigger = rssBeforeMb >= policy.rssThresholdMb;
97
- const requestTrigger = gcState.requestsSinceLastRun >= policy.maxRequestsWithoutCollection;
98
-
99
- if (!force && !rssTrigger && !requestTrigger) {
100
- return false;
101
- }
102
-
103
- const now = Date.now();
104
- if (
105
- !force &&
106
- policy.minIntervalMs > 0 &&
107
- gcState.lastRunAtMs > 0 &&
108
- now - gcState.lastRunAtMs < policy.minIntervalMs
109
- ) {
110
- return false;
111
- }
112
-
113
- global.gc();
114
- const after = process.memoryUsage();
115
- gcState.lastRunAtMs = now;
116
- gcState.requestsSinceLastRun = 0;
117
-
118
- let trigger = 'forced';
119
- if (!force) {
120
- if (rssTrigger && requestTrigger) trigger = 'rss+requests';
121
- else if (rssTrigger) trigger = 'rss';
122
- else trigger = 'requests';
123
- }
124
-
125
- log(
126
- `[Child:${process.pid}] GC ${reason}: trigger=${trigger} rss ${(before.rss / 1024 / 1024).toFixed(1)}MB -> ${(after.rss / 1024 / 1024).toFixed(1)}MB`
127
- );
128
- return true;
129
- }
130
-
131
- function ensureNativeBackend(threads) {
132
- if (nativeBackendConfigured && !threads) return;
133
- configureNativeOnnxBackend({
134
- log,
135
- label: '[Child]',
136
- threads,
137
- });
138
- nativeBackendConfigured = true;
139
- }
140
-
141
- function setThreads(numThreads) {
142
- ensureNativeBackend({
143
- intraOpNumThreads: numThreads,
144
- interOpNumThreads: 1,
145
- });
146
- configuredThreads = numThreads;
147
- }
148
-
149
- async function getEmbedder(embeddingModel, numThreads) {
150
- if (!embedderPromise) {
151
- configuredModel = embeddingModel;
152
- setThreads(numThreads);
153
- env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
154
- log(`Loading model ${embeddingModel}...`);
155
- const loadStart = Date.now();
156
- embedderPromise = pipeline('feature-extraction', embeddingModel, {
157
- quantized: true,
158
- dtype: 'fp32',
159
- session_options: {
160
- numThreads,
161
- intraOpNumThreads: numThreads,
162
- interOpNumThreads: 1,
163
- },
164
- }).then((model) => {
165
- const loadSec = ((Date.now() - loadStart) / 1000).toFixed(1);
166
- log(`Model ready in ${loadSec}s, ${formatMemory()}`);
167
- return model;
168
- });
169
- } else if (configuredModel && embeddingModel !== configuredModel) {
170
- log(`Model changed (${configuredModel} -> ${embeddingModel}); reloading embedder`);
171
- embedderPromise = null;
172
- configuredModel = null;
173
- return getEmbedder(embeddingModel, numThreads);
174
- } else if (configuredThreads !== null && numThreads !== configuredThreads) {
175
- log(`Warning: numThreads changed (${configuredThreads} -> ${numThreads})`);
176
- }
177
-
178
- return embedderPromise;
179
- }
180
-
181
- function resetEmbeddingProcessState() {
182
- embedderPromise = null;
183
- configuredThreads = null;
184
- configuredModel = null;
185
- requestCounter = 0;
186
- currentRequestId = -1;
187
- nativeBackendConfigured = false;
188
- gcState.lastRunAtMs = 0;
189
- gcState.requestsSinceLastRun = 0;
190
- }
191
-
192
-
193
- async function unloadModel() {
194
- if (!embedderPromise) {
195
- log('[Child] No model loaded, nothing to unload');
196
- return { success: true, wasLoaded: false };
197
- }
198
-
199
- try {
200
- const embedder = await embedderPromise;
201
-
202
-
203
- if (embedder && typeof embedder.dispose === 'function') {
204
- try {
205
- await embedder.dispose();
206
- log('[Child] Model disposed successfully');
207
- } catch (disposeErr) {
208
- log(`[Child] Model dispose warning: ${disposeErr.message}`);
209
- }
210
- }
211
- } catch (err) {
212
- log(`[Child] Error during model unload: ${err.message}`);
213
- }
214
-
215
-
216
- embedderPromise = null;
217
- configuredModel = null;
218
- configuredThreads = null;
219
-
220
-
221
- if (gcSupported) {
222
- maybeRunGc(resolveGcPolicy(), { reason: 'post-unload', force: true });
223
- }
224
-
225
- log(`[Child] Model unloaded, ${formatMemory()}`);
226
- return { success: true, wasLoaded: true };
227
- }
228
-
229
- async function runEmbedding(payload) {
230
- const {
231
- embeddingModel,
232
- chunks = [],
233
- numThreads = 1,
234
- batchSize = null,
235
- enableExplicitGc = true,
236
- gcRssThresholdMb = EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
237
- gcMinIntervalMs = EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
238
- gcMaxRequestsWithoutCollection = EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
239
- requestId = null,
240
- } = payload || {};
241
- const shouldRunGc = enableExplicitGc !== false && gcSupported;
242
- const gcPolicy = resolveGcPolicy({
243
- gcRssThresholdMb,
244
- gcMinIntervalMs,
245
- gcMaxRequestsWithoutCollection,
246
- });
247
-
248
- if (!embeddingModel) {
249
- throw new Error('Missing embeddingModel');
250
- }
251
-
252
- const reqId = requestId ?? requestCounter++;
253
- currentRequestId = reqId;
254
- const embedder = await getEmbedder(embeddingModel, numThreads);
255
- log(`Request ${reqId}: embedding ${chunks.length} chunks, ${formatMemory()}`);
256
-
257
- const results = [];
258
- let disposeCount = 0;
259
- const start = Date.now();
260
- if (shouldRunGc) {
261
- gcState.requestsSinceLastRun += 1;
262
- }
263
-
264
-
265
-
266
- const BATCH_SIZE =
267
- Number.isInteger(batchSize) && batchSize > 0 ? Math.min(batchSize, 256) : 1;
268
-
269
- for (let batchStart = 0; batchStart < chunks.length; batchStart += BATCH_SIZE) {
270
- const batchEnd = Math.min(batchStart + BATCH_SIZE, chunks.length);
271
- const batchChunks = chunks.slice(batchStart, batchEnd);
272
- const batchTexts = batchChunks.map((c) => c.text);
273
-
274
- try {
275
-
276
- const output = await embedder(batchTexts, { pooling: 'mean', normalize: true });
277
-
278
-
279
- const hiddenSize = output.dims[output.dims.length - 1];
280
-
281
- for (let j = 0; j < batchChunks.length; j++) {
282
- const chunk = batchChunks[j];
283
- const vecStart = j * hiddenSize;
284
- const vecEnd = vecStart + hiddenSize;
285
-
286
- const vector = new Float32Array(output.data.subarray(vecStart, vecEnd));
287
-
288
- results.push({
289
- file: chunk.file,
290
- startLine: chunk.startLine,
291
- endLine: chunk.endLine,
292
- content: chunk.text,
293
- vector: Array.from(vector),
294
- success: true,
295
- });
296
- }
297
-
298
-
299
- if (typeof output.dispose === 'function') {
300
- try {
301
- output.dispose();
302
- } catch {
303
-
304
- }
305
- }
306
- disposeCount++;
307
- } catch (error) {
308
-
309
- log(`Batch failed, falling back to single: ${error.message}`);
310
- for (const chunk of batchChunks) {
311
- try {
312
- const output = await embedder(chunk.text, { pooling: 'mean', normalize: true });
313
- const vector = new Float32Array(output.data);
314
- if (typeof output.dispose === 'function') {
315
- try {
316
- output.dispose();
317
- } catch {
318
-
319
- }
320
- }
321
- disposeCount++;
322
- results.push({
323
- file: chunk.file,
324
- startLine: chunk.startLine,
325
- endLine: chunk.endLine,
326
- content: chunk.text,
327
- vector: Array.from(vector),
328
- success: true,
329
- });
330
- } catch (innerErr) {
331
- results.push({
332
- file: chunk.file,
333
- startLine: chunk.startLine,
334
- endLine: chunk.endLine,
335
- error: innerErr.message,
336
- success: false,
337
- });
338
- }
339
- }
340
- }
341
-
342
-
343
- if (batchEnd % 20 === 0 || batchEnd === chunks.length) {
344
- const elapsed = ((Date.now() - start) / 1000).toFixed(1);
345
- log(
346
- `[Child:${process.pid}] Request ${reqId}: processed ${batchEnd}/${chunks.length} chunks in ${elapsed}s, ${formatMemory()}`
347
- );
348
- }
349
-
350
- if (shouldRunGc && (batchEnd % 20 === 0 || batchEnd === chunks.length)) {
351
- maybeRunGc(gcPolicy, {
352
- reason: `request ${reqId} progress ${batchEnd}/${chunks.length}`,
353
- });
354
- }
355
- }
356
-
357
- const totalSec = ((Date.now() - start) / 1000).toFixed(1);
358
- log(
359
- `[Child:${process.pid}] Request ${reqId}: done ${results.length} chunks in ${totalSec}s, ${disposeCount} tensors disposed, ${formatMemory()}`
360
- );
361
- if (shouldRunGc) {
362
- maybeRunGc(gcPolicy, { reason: `request ${reqId} end` });
363
- }
364
- const usage = process.memoryUsage();
365
- return {
366
- results,
367
- meta: {
368
- rssMb: usage.rss / 1024 / 1024,
369
- heapMb: usage.heapUsed / 1024 / 1024,
370
- heapTotalMb: usage.heapTotal / 1024 / 1024,
371
- },
372
- };
373
- }
374
-
375
- async function main() {
376
- log(`[Child:${process.pid}] Starting, ${formatMemory()}`);
377
-
378
- if (persistent) {
379
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
380
- let chain = Promise.resolve();
381
-
382
- rl.on('line', (line) => {
383
- const trimmed = line.trim();
384
- if (!trimmed) return;
385
-
386
- let payload;
387
- try {
388
- payload = JSON.parse(trimmed);
389
- } catch (err) {
390
- log(`[Child:${process.pid}] Failed to parse payload: ${err.message}`);
391
- process.stdout.write(`${JSON.stringify({ results: [] })}\n`);
392
- return;
393
- }
394
-
395
- if (payload?.type === 'shutdown') {
396
- rl.close();
397
- process.exit(0);
398
- return;
399
- }
400
-
401
- if (payload?.type === 'unload') {
402
- chain = chain
403
- .then(() => unloadModel())
404
- .then((result) => {
405
- process.stdout.write(`${JSON.stringify(result)}\n`);
406
- })
407
- .catch((err) => {
408
- log(`[Child:${process.pid}] Error unloading model: ${err.message}`);
409
- process.stdout.write(`${JSON.stringify({ success: false, error: err.message })}\n`);
410
- });
411
- return;
412
- }
413
-
414
- chain = chain
415
- .then(() => runEmbedding(payload))
416
- .then((output) => {
417
- process.stdout.write(`${JSON.stringify(output)}\n`);
418
- })
419
- .catch((err) => {
420
- log(`[Child:${process.pid}] Error processing payload: ${err.message}`);
421
- process.stdout.write(`${JSON.stringify({ results: [] })}\n`);
422
- });
423
- });
424
- return;
425
- }
426
-
427
- const raw = await readStdin();
428
- if (!raw) return;
429
-
430
- const payload = JSON.parse(raw);
431
- const output = await runEmbedding(payload);
432
- process.stdout.write(JSON.stringify(output));
433
- }
434
-
435
- function shouldRunMain() {
436
- if (process.env.EMBEDDING_PROCESS_RUN_MAIN === 'true') return true;
437
- if (process.env.VITEST) return false;
438
- if (!process.argv[1]) return false;
439
- const entryUrl = pathToFileURL(process.argv[1]).href;
440
- return import.meta.url === entryUrl;
441
- }
442
-
443
- if (shouldRunMain()) {
444
- main().catch((err) => {
445
- log(`[Child:${process.pid}] Error: ${err?.message || err}`);
446
- process.stderr.write(String(err?.message || err));
447
- process.exit(1);
448
- });
449
- }
450
-
451
- export { getEmbedder, resetEmbeddingProcessState, unloadModel };
1
+ import { pipeline, env } from '@huggingface/transformers';
2
+ import { configureNativeOnnxBackend } from './onnx-backend.js';
3
+ import {
4
+ EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
5
+ EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
6
+ EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
7
+ EMBEDDING_PROCESS_GC_STATE_INITIAL,
8
+ } from './constants.js';
9
+ import readline from 'readline';
10
+ import path from 'path';
11
+ import os from 'os';
12
+ import { pathToFileURL } from 'url';
13
+
14
+ let currentRequestId = -1;
15
+ const log = (...args) => {
16
+ if (currentRequestId > 0 && !process.env.EMBEDDING_PROCESS_VERBOSE) {
17
+ return;
18
+ }
19
+ console.error(...args);
20
+ };
21
+
22
+ function formatMemory() {
23
+ const usage = process.memoryUsage();
24
+ return `rss=${(usage.rss / 1024 / 1024).toFixed(1)}MB heap=${(usage.heapUsed / 1024 / 1024).toFixed(1)}MB`;
25
+ }
26
+
27
+ function readStdin() {
28
+ return new Promise((resolve, reject) => {
29
+ let data = '';
30
+ process.stdin.setEncoding('utf8');
31
+ process.stdin.on('data', (chunk) => {
32
+ data += chunk;
33
+ });
34
+ process.stdin.on('end', () => resolve(data));
35
+ process.stdin.on('error', reject);
36
+ });
37
+ }
38
+
39
+ const persistent = process.env.EMBEDDING_PROCESS_PERSISTENT === 'true';
40
+ let embedderPromise = null;
41
+ let configuredThreads = null;
42
+ let configuredModel = null;
43
+ let requestCounter = 0;
44
+ let gcSupported = typeof global.gc === 'function';
45
+ let nativeBackendConfigured = false;
46
+ const gcState = { ...EMBEDDING_PROCESS_GC_STATE_INITIAL };
47
+
48
+ function getGlobalCacheDir() {
49
+ if (process.platform === 'win32') {
50
+ return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
51
+ }
52
+ if (process.platform === 'darwin') {
53
+ return path.join(os.homedir(), 'Library', 'Caches');
54
+ }
55
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
56
+ }
57
+
58
+ function toPositiveNumber(value, fallback) {
59
+ const parsed = Number(value);
60
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
61
+ }
62
+
63
+ function toNonNegativeInteger(value, fallback) {
64
+ const parsed = Number.parseInt(value, 10);
65
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
66
+ }
67
+
68
+ function toPositiveInteger(value, fallback) {
69
+ const parsed = Number.parseInt(value, 10);
70
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
71
+ }
72
+
73
+ function resolveGcPolicy(payload) {
74
+ return {
75
+ rssThresholdMb: toPositiveNumber(
76
+ payload?.gcRssThresholdMb,
77
+ EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB
78
+ ),
79
+ minIntervalMs: toNonNegativeInteger(
80
+ payload?.gcMinIntervalMs,
81
+ EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS
82
+ ),
83
+ maxRequestsWithoutCollection: toPositiveInteger(
84
+ payload?.gcMaxRequestsWithoutCollection,
85
+ EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION
86
+ ),
87
+ };
88
+ }
89
+
90
+ function maybeRunGc(policy, { reason = 'unknown', force = false } = {}) {
91
+ if (!gcSupported) return false;
92
+
93
+ const before = process.memoryUsage();
94
+ const rssBeforeMb = before.rss / 1024 / 1024;
95
+ const rssTrigger = rssBeforeMb >= policy.rssThresholdMb;
96
+ const requestTrigger = gcState.requestsSinceLastRun >= policy.maxRequestsWithoutCollection;
97
+
98
+ if (!force && !rssTrigger && !requestTrigger) {
99
+ return false;
100
+ }
101
+
102
+ const now = Date.now();
103
+ if (
104
+ !force &&
105
+ policy.minIntervalMs > 0 &&
106
+ gcState.lastRunAtMs > 0 &&
107
+ now - gcState.lastRunAtMs < policy.minIntervalMs
108
+ ) {
109
+ return false;
110
+ }
111
+
112
+ global.gc();
113
+ const after = process.memoryUsage();
114
+ gcState.lastRunAtMs = now;
115
+ gcState.requestsSinceLastRun = 0;
116
+
117
+ let trigger = 'forced';
118
+ if (!force) {
119
+ if (rssTrigger && requestTrigger) trigger = 'rss+requests';
120
+ else if (rssTrigger) trigger = 'rss';
121
+ else trigger = 'requests';
122
+ }
123
+
124
+ log(
125
+ `[Child:${process.pid}] GC ${reason}: trigger=${trigger} rss ${(before.rss / 1024 / 1024).toFixed(1)}MB -> ${(after.rss / 1024 / 1024).toFixed(1)}MB`
126
+ );
127
+ return true;
128
+ }
129
+
130
+ function ensureNativeBackend(threads) {
131
+ if (nativeBackendConfigured && !threads) return;
132
+ configureNativeOnnxBackend({
133
+ log,
134
+ label: '[Child]',
135
+ threads,
136
+ });
137
+ nativeBackendConfigured = true;
138
+ }
139
+
140
+ function setThreads(numThreads) {
141
+ ensureNativeBackend({
142
+ intraOpNumThreads: numThreads,
143
+ interOpNumThreads: 1,
144
+ });
145
+ configuredThreads = numThreads;
146
+ }
147
+
148
+ async function getEmbedder(embeddingModel, numThreads) {
149
+ if (!embedderPromise) {
150
+ configuredModel = embeddingModel;
151
+ setThreads(numThreads);
152
+ env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
153
+ log(`Loading model ${embeddingModel}...`);
154
+ const loadStart = Date.now();
155
+ embedderPromise = pipeline('feature-extraction', embeddingModel, {
156
+ quantized: true,
157
+ dtype: 'fp32',
158
+ session_options: {
159
+ numThreads,
160
+ intraOpNumThreads: numThreads,
161
+ interOpNumThreads: 1,
162
+ },
163
+ }).then((model) => {
164
+ const loadSec = ((Date.now() - loadStart) / 1000).toFixed(1);
165
+ log(`Model ready in ${loadSec}s, ${formatMemory()}`);
166
+ return model;
167
+ });
168
+ } else if (configuredModel && embeddingModel !== configuredModel) {
169
+ log(`Model changed (${configuredModel} -> ${embeddingModel}); reloading embedder`);
170
+ embedderPromise = null;
171
+ configuredModel = null;
172
+ return getEmbedder(embeddingModel, numThreads);
173
+ } else if (configuredThreads !== null && numThreads !== configuredThreads) {
174
+ log(`Warning: numThreads changed (${configuredThreads} -> ${numThreads})`);
175
+ }
176
+
177
+ return embedderPromise;
178
+ }
179
+
180
+ function resetEmbeddingProcessState() {
181
+ embedderPromise = null;
182
+ configuredThreads = null;
183
+ configuredModel = null;
184
+ requestCounter = 0;
185
+ currentRequestId = -1;
186
+ nativeBackendConfigured = false;
187
+ gcState.lastRunAtMs = 0;
188
+ gcState.requestsSinceLastRun = 0;
189
+ }
190
+
191
+ async function unloadModel() {
192
+ if (!embedderPromise) {
193
+ log('[Child] No model loaded, nothing to unload');
194
+ return { success: true, wasLoaded: false };
195
+ }
196
+
197
+ try {
198
+ const embedder = await embedderPromise;
199
+
200
+ if (embedder && typeof embedder.dispose === 'function') {
201
+ try {
202
+ await embedder.dispose();
203
+ log('[Child] Model disposed successfully');
204
+ } catch (disposeErr) {
205
+ log(`[Child] Model dispose warning: ${disposeErr.message}`);
206
+ }
207
+ }
208
+ } catch (err) {
209
+ log(`[Child] Error during model unload: ${err.message}`);
210
+ }
211
+
212
+ embedderPromise = null;
213
+ configuredModel = null;
214
+ configuredThreads = null;
215
+
216
+ if (gcSupported) {
217
+ maybeRunGc(resolveGcPolicy(), { reason: 'post-unload', force: true });
218
+ }
219
+
220
+ log(`[Child] Model unloaded, ${formatMemory()}`);
221
+ return { success: true, wasLoaded: true };
222
+ }
223
+
224
+ async function runEmbedding(payload) {
225
+ const {
226
+ embeddingModel,
227
+ chunks = [],
228
+ numThreads = 1,
229
+ batchSize = null,
230
+ enableExplicitGc = true,
231
+ gcRssThresholdMb = EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
232
+ gcMinIntervalMs = EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
233
+ gcMaxRequestsWithoutCollection = EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
234
+ requestId = null,
235
+ } = payload || {};
236
+ const shouldRunGc = enableExplicitGc !== false && gcSupported;
237
+ const gcPolicy = resolveGcPolicy({
238
+ gcRssThresholdMb,
239
+ gcMinIntervalMs,
240
+ gcMaxRequestsWithoutCollection,
241
+ });
242
+
243
+ if (!embeddingModel) {
244
+ throw new Error('Missing embeddingModel');
245
+ }
246
+
247
+ const reqId = requestId ?? requestCounter++;
248
+ currentRequestId = reqId;
249
+ const embedder = await getEmbedder(embeddingModel, numThreads);
250
+ log(`Request ${reqId}: embedding ${chunks.length} chunks, ${formatMemory()}`);
251
+
252
+ const results = [];
253
+ let disposeCount = 0;
254
+ const start = Date.now();
255
+ if (shouldRunGc) {
256
+ gcState.requestsSinceLastRun += 1;
257
+ }
258
+
259
+ const BATCH_SIZE = Number.isInteger(batchSize) && batchSize > 0 ? Math.min(batchSize, 256) : 1;
260
+
261
+ for (let batchStart = 0; batchStart < chunks.length; batchStart += BATCH_SIZE) {
262
+ const batchEnd = Math.min(batchStart + BATCH_SIZE, chunks.length);
263
+ const batchChunks = chunks.slice(batchStart, batchEnd);
264
+ const batchTexts = batchChunks.map((c) => c.text);
265
+
266
+ try {
267
+ const output = await embedder(batchTexts, { pooling: 'mean', normalize: true });
268
+
269
+ const hiddenSize = output.dims[output.dims.length - 1];
270
+
271
+ for (let j = 0; j < batchChunks.length; j++) {
272
+ const chunk = batchChunks[j];
273
+ const vecStart = j * hiddenSize;
274
+ const vecEnd = vecStart + hiddenSize;
275
+
276
+ const vector = new Float32Array(output.data.subarray(vecStart, vecEnd));
277
+
278
+ results.push({
279
+ file: chunk.file,
280
+ startLine: chunk.startLine,
281
+ endLine: chunk.endLine,
282
+ content: chunk.text,
283
+ vector: Array.from(vector),
284
+ success: true,
285
+ });
286
+ }
287
+
288
+ if (typeof output.dispose === 'function') {
289
+ try {
290
+ output.dispose();
291
+ } catch {}
292
+ }
293
+ disposeCount++;
294
+ } catch (error) {
295
+ log(`Batch failed, falling back to single: ${error.message}`);
296
+ for (const chunk of batchChunks) {
297
+ try {
298
+ const output = await embedder(chunk.text, { pooling: 'mean', normalize: true });
299
+ const vector = new Float32Array(output.data);
300
+ if (typeof output.dispose === 'function') {
301
+ try {
302
+ output.dispose();
303
+ } catch {}
304
+ }
305
+ disposeCount++;
306
+ results.push({
307
+ file: chunk.file,
308
+ startLine: chunk.startLine,
309
+ endLine: chunk.endLine,
310
+ content: chunk.text,
311
+ vector: Array.from(vector),
312
+ success: true,
313
+ });
314
+ } catch (innerErr) {
315
+ results.push({
316
+ file: chunk.file,
317
+ startLine: chunk.startLine,
318
+ endLine: chunk.endLine,
319
+ error: innerErr.message,
320
+ success: false,
321
+ });
322
+ }
323
+ }
324
+ }
325
+
326
+ if (batchEnd % 20 === 0 || batchEnd === chunks.length) {
327
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
328
+ log(
329
+ `[Child:${process.pid}] Request ${reqId}: processed ${batchEnd}/${chunks.length} chunks in ${elapsed}s, ${formatMemory()}`
330
+ );
331
+ }
332
+
333
+ if (shouldRunGc && (batchEnd % 20 === 0 || batchEnd === chunks.length)) {
334
+ maybeRunGc(gcPolicy, {
335
+ reason: `request ${reqId} progress ${batchEnd}/${chunks.length}`,
336
+ });
337
+ }
338
+ }
339
+
340
+ const totalSec = ((Date.now() - start) / 1000).toFixed(1);
341
+ log(
342
+ `[Child:${process.pid}] Request ${reqId}: done ${results.length} chunks in ${totalSec}s, ${disposeCount} tensors disposed, ${formatMemory()}`
343
+ );
344
+ if (shouldRunGc) {
345
+ maybeRunGc(gcPolicy, { reason: `request ${reqId} end` });
346
+ }
347
+ const usage = process.memoryUsage();
348
+ return {
349
+ results,
350
+ meta: {
351
+ rssMb: usage.rss / 1024 / 1024,
352
+ heapMb: usage.heapUsed / 1024 / 1024,
353
+ heapTotalMb: usage.heapTotal / 1024 / 1024,
354
+ },
355
+ };
356
+ }
357
+
358
+ async function main() {
359
+ log(`[Child:${process.pid}] Starting, ${formatMemory()}`);
360
+
361
+ if (persistent) {
362
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
363
+ let chain = Promise.resolve();
364
+
365
+ rl.on('line', (line) => {
366
+ const trimmed = line.trim();
367
+ if (!trimmed) return;
368
+
369
+ let payload;
370
+ try {
371
+ payload = JSON.parse(trimmed);
372
+ } catch (err) {
373
+ log(`[Child:${process.pid}] Failed to parse payload: ${err.message}`);
374
+ process.stdout.write(`${JSON.stringify({ results: [] })}\n`);
375
+ return;
376
+ }
377
+
378
+ if (payload?.type === 'shutdown') {
379
+ rl.close();
380
+ process.exit(0);
381
+ return;
382
+ }
383
+
384
+ if (payload?.type === 'unload') {
385
+ chain = chain
386
+ .then(() => unloadModel())
387
+ .then((result) => {
388
+ process.stdout.write(`${JSON.stringify(result)}\n`);
389
+ })
390
+ .catch((err) => {
391
+ log(`[Child:${process.pid}] Error unloading model: ${err.message}`);
392
+ process.stdout.write(`${JSON.stringify({ success: false, error: err.message })}\n`);
393
+ });
394
+ return;
395
+ }
396
+
397
+ chain = chain
398
+ .then(() => runEmbedding(payload))
399
+ .then((output) => {
400
+ process.stdout.write(`${JSON.stringify(output)}\n`);
401
+ })
402
+ .catch((err) => {
403
+ log(`[Child:${process.pid}] Error processing payload: ${err.message}`);
404
+ process.stdout.write(`${JSON.stringify({ results: [] })}\n`);
405
+ });
406
+ });
407
+ return;
408
+ }
409
+
410
+ const raw = await readStdin();
411
+ if (!raw) return;
412
+
413
+ const payload = JSON.parse(raw);
414
+ const output = await runEmbedding(payload);
415
+ process.stdout.write(JSON.stringify(output));
416
+ }
417
+
418
+ function shouldRunMain() {
419
+ if (process.env.EMBEDDING_PROCESS_RUN_MAIN === 'true') return true;
420
+ if (process.env.VITEST) return false;
421
+ if (!process.argv[1]) return false;
422
+ const entryUrl = pathToFileURL(process.argv[1]).href;
423
+ return import.meta.url === entryUrl;
424
+ }
425
+
426
+ if (shouldRunMain()) {
427
+ main().catch((err) => {
428
+ log(`[Child:${process.pid}] Error: ${err?.message || err}`);
429
+ process.stderr.write(String(err?.message || err));
430
+ process.exit(1);
431
+ });
432
+ }
433
+
434
+ export { getEmbedder, resetEmbeddingProcessState, unloadModel };