@softerist/heuristic-mcp 3.0.12 → 3.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.js CHANGED
@@ -5,8 +5,127 @@ import crypto from 'crypto';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { ProjectDetector } from './project-detector.js';
7
7
  import { parseJsonc } from './settings-editor.js';
8
-
9
- const DEFAULT_CONFIG = {
8
+ import {
9
+ EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION,
10
+ EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS,
11
+ EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB,
12
+ } from './constants.js';
13
+
14
+ const DEFAULT_MEMORY_CLEANUP_CONFIG = {
15
+ enableExplicitGc: true, // Require --expose-gc for more aggressive memory cleanup
16
+ clearCacheAfterIndex: true, // Drop in-memory vectors after indexing completes
17
+ unloadModelAfterIndex: true, // Unload embedding model from memory after indexing completes to free RAM
18
+ shutdownQueryEmbeddingPoolAfterIndex: true, // Force shutdown search embedding child pool after index operations
19
+ unloadModelAfterSearch: true, // Unload embedding model after search queries to keep memory low (trades speed for RAM)
20
+ embeddingPoolIdleTimeoutMs: 2000, // Idle timeout before killing persistent embedding child process (ms)
21
+ incrementalGcThresholdMb: 512, // RSS threshold for optional incremental GC
22
+ incrementalMemoryProfile: false, // Enable phase-level incremental indexing memory traces (diagnostics)
23
+ recycleServerOnHighRssAfterIncremental: false, // Recycle server process after incremental cleanup if RSS remains high
24
+ recycleServerOnHighRssThresholdMb: 4096, // RSS threshold (MB) that triggers incremental recycle
25
+ recycleServerOnHighRssCooldownMs: 300000, // Minimum interval between recycle attempts
26
+ recycleServerOnHighRssDelayMs: 2000, // Delay before recycle to allow logs/responses to flush
27
+ };
28
+
29
+ const DEFAULT_INDEXING_CONFIG = {
30
+ smartIndexing: true, // Enable automatic project type detection and smart ignore patterns
31
+ chunkSize: 16, // Lines per chunk (tuned for speed/memory balance)
32
+ chunkOverlap: 4, // Overlap between chunks for context continuity
33
+ batchSize: 50, // Number of files to process in a single indexing batch
34
+ maxFileSize: 1048576, // 1MB - skip files larger than this
35
+ prefilterContentMaxBytes: 512 * 1024, // 512KB - cache content during prefilter to avoid double reads
36
+ maxResults: 5, // Maximum number of semantic search results to return
37
+ watchFiles: true, // Enable file system watcher to re-index changed files in real-time
38
+ };
39
+
40
+ const DEFAULT_LOGGING_CONFIG = {
41
+ verbose: false, // Enable detailed logging for debugging and progress tracking
42
+ memoryLogIntervalMs: 5000, // Verbose memory log cadence during indexing (ms)
43
+ };
44
+
45
+ const DEFAULT_CACHE_CONFIG = {
46
+ enableCache: true, // Whether to persist and reload embeddings between sessions
47
+ saveReaderWaitTimeoutMs: 5000, // Max wait for active reads before saving binary cache
48
+ cacheVectorAssumeFinite: true, // Assume vectors are finite (skip validation)
49
+ cacheVectorFloatDigits: null, // Decimal precision for cached vectors (null = default)
50
+ cacheWriteHighWaterMark: 262144, // Write stream highWaterMark for cache files
51
+ cacheVectorFlushChars: 262144, // Flush threshold (chars) for JSON writer
52
+ cacheVectorCheckFinite: true, // Validate vectors contain only finite numbers
53
+ cacheVectorNoMutation: false, // Avoid mutating vectors during serialization
54
+ cacheVectorJoinThreshold: 8192, // Join threshold for JSON array chunks
55
+ cacheVectorJoinChunkSize: 2048, // Chunk size for JSON join optimization
56
+ };
57
+
58
+ const DEFAULT_WORKER_CONFIG = {
59
+ workerThreads: 'auto', // 0 = run in main thread (no workers), "auto" = CPU cores - 1, or set a number
60
+ workerBatchTimeoutMs: 120000, // Timeout per worker batch before fallback (ms)
61
+ workerFailureThreshold: 1, // Open circuit after N worker failures
62
+ workerFailureCooldownMs: 10 * 60 * 1000, // Cooldown before retrying workers
63
+ workerMaxChunksPerBatch: 100, // Cap chunks per worker batch to reduce hang risk
64
+ allowSingleThreadFallback: false, // Allow fallback to main-thread embeddings if workers fail
65
+ failFastEmbeddingErrors: false, // Abort worker embedding batch after repeated consecutive embed failures
66
+ };
67
+
68
+ const DEFAULT_EMBEDDING_CONFIG = {
69
+ embeddingModel: 'jinaai/jina-embeddings-v2-base-code', // AI model ID used for semantic search
70
+ embeddingDimension: null, // null = full dimensions, or 64/128/256/512/768 for MRL-trained models
71
+ preloadEmbeddingModel: true, // Preload the embedding model at startup (server mode)
72
+ embeddingProcessPerBatch: false, // Use child process per batch for memory isolation
73
+ autoEmbeddingProcessPerBatch: true, // Auto-enable child process embedding in single-threaded mode for heavy models
74
+ embeddingBatchSize: null, // Override embedding batch size (null = auto)
75
+ embeddingProcessNumThreads: 8, // ONNX threads used by embedding child process
76
+ embeddingProcessGcRssThresholdMb: EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB, // RSS threshold for embedding-child adaptive GC
77
+ embeddingProcessGcMinIntervalMs: EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS, // Minimum interval between embedding-child GC runs
78
+ embeddingProcessGcMaxRequestsWithoutCollection:
79
+ EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION, // Backstop GC cadence for embedding child
80
+ };
81
+
82
+ const DEFAULT_VECTOR_STORE_CONFIG = {
83
+ vectorStoreFormat: 'binary', // json | binary | sqlite (binary uses mmap-friendly on-disk store)
84
+ vectorStoreContentMode: 'external', // external = content loaded on-demand for binary store
85
+ contentCacheEntries: 256, // In-memory content cache entries for binary store
86
+ vectorStoreLoadMode: 'memory', // memory | disk (disk streams vectors from disk / memory is faster but requires more RAM)
87
+ vectorCacheEntries: 0, // In-memory vector cache entries for disk-backed loads
88
+ };
89
+
90
+ const DEFAULT_SEARCH_CONFIG = {
91
+ semanticWeight: 0.7, // Balance between semantic and keyword scores (0.0 to 1.0)
92
+ exactMatchBoost: 1.5, // Multiplier applied when an exact string match is found
93
+ recencyBoost: 0.1, // Boost for recently modified files (max 0.1 added to score)
94
+ recencyDecayDays: 30, // After this many days, recency boost is 0
95
+ textMatchMaxCandidates: 2000, // Max candidates for full text matching before deferring
96
+ };
97
+
98
+ const DEFAULT_CALL_GRAPH_CONFIG = {
99
+ callGraphEnabled: true, // Enable call graph extraction for proximity boosting
100
+ callGraphBoost: 0.15, // Boost for files related via call graph (0-1)
101
+ callGraphMaxHops: 1, // How many levels of calls to follow (1 = direct only)
102
+ };
103
+
104
+ const DEFAULT_ANN_CONFIG = {
105
+ annEnabled: true, // Enable Approximate Nearest Neighbor (ANN) index for large codebases
106
+ annMinChunks: 5000, // Minimum number of chunks required to trigger ANN indexing
107
+ annMinCandidates: 50, // Minimum initial candidates to pull from ANN before refinement
108
+ annMaxCandidates: 200, // Hard limit on the number of ANN candidates to process
109
+ annCandidateMultiplier: 20, // Scale initial search depth based on requested maxResults
110
+ annEfConstruction: 200, // HNSW index construction quality (higher = better index, slower build)
111
+ annEfSearch: 64, // HNSW search parameter (higher = more accurate, slower search)
112
+ annM: 16, // Number of connections per element in HNSW index
113
+ annIndexCache: true, // Whether to cache the built HNSW index on disk
114
+ annMetric: 'cosine', // Distance metric for similarity (currently locked to cosine)
115
+ };
116
+
117
+ const MEMORY_CLEANUP_KEYS = Object.freeze(Object.keys(DEFAULT_MEMORY_CLEANUP_CONFIG));
118
+ const INDEXING_KEYS = Object.freeze(Object.keys(DEFAULT_INDEXING_CONFIG));
119
+ const LOGGING_KEYS = Object.freeze(Object.keys(DEFAULT_LOGGING_CONFIG));
120
+ const CACHE_KEYS = Object.freeze(Object.keys(DEFAULT_CACHE_CONFIG));
121
+ const WORKER_KEYS = Object.freeze(Object.keys(DEFAULT_WORKER_CONFIG));
122
+ const EMBEDDING_KEYS = Object.freeze(Object.keys(DEFAULT_EMBEDDING_CONFIG));
123
+ const VECTOR_STORE_KEYS = Object.freeze(Object.keys(DEFAULT_VECTOR_STORE_CONFIG));
124
+ const SEARCH_KEYS = Object.freeze(Object.keys(DEFAULT_SEARCH_CONFIG));
125
+ const CALL_GRAPH_KEYS = Object.freeze(Object.keys(DEFAULT_CALL_GRAPH_CONFIG));
126
+ const ANN_KEYS = Object.freeze(Object.keys(DEFAULT_ANN_CONFIG));
127
+
128
+ const DEFAULT_CONFIG = {
10
129
  searchDirectory: '.',
11
130
  fileExtensions: [
12
131
  // JavaScript/TypeScript
@@ -242,16 +361,16 @@ const DEFAULT_CONFIG = {
242
361
  '**/scripts/**',
243
362
  '**/tools/**',
244
363
  ],
245
- chunkSize: 16, // Lines per chunk (tuned for speed/memory balance)
246
- chunkOverlap: 4, // Overlap between chunks for context continuity
247
- batchSize: 50, // Number of files to process in a single indexing batch
248
- maxFileSize: 1048576, // 1MB - skip files larger than this
249
- prefilterContentMaxBytes: 512 * 1024, // 512KB - cache content during prefilter to avoid double reads
250
- maxResults: 5, // Maximum number of semantic search results to return
251
- enableCache: true, // Whether to persist and reload embeddings between sessions
252
- cacheDirectory: null, // Will be set dynamically by loadConfig()
253
- // Cache cleanup behavior (consolidated namespace)
254
- cacheCleanup: {
364
+ chunkSize: DEFAULT_INDEXING_CONFIG.chunkSize,
365
+ chunkOverlap: DEFAULT_INDEXING_CONFIG.chunkOverlap,
366
+ batchSize: DEFAULT_INDEXING_CONFIG.batchSize,
367
+ maxFileSize: DEFAULT_INDEXING_CONFIG.maxFileSize,
368
+ prefilterContentMaxBytes: DEFAULT_INDEXING_CONFIG.prefilterContentMaxBytes,
369
+ maxResults: DEFAULT_INDEXING_CONFIG.maxResults,
370
+ enableCache: DEFAULT_CACHE_CONFIG.enableCache,
371
+ cacheDirectory: null, // Will be set dynamically by loadConfig()
372
+ // Cache cleanup behavior (consolidated namespace)
373
+ cacheCleanup: {
255
374
  autoCleanup: true, // Automatically remove stale caches on startup
256
375
  staleNoMetaHours: 6, // Hours before incomplete cache (no meta.json) is considered stale
257
376
  emptyThresholdHours: 24, // Hours before empty cache (0 files/chunks) is removed
@@ -259,56 +378,82 @@ const DEFAULT_CONFIG = {
259
378
  maxUnusedDays: 30, // Days before unused cache is removed
260
379
  tempThresholdHours: 24, // Hours before temp workspace cache is removed
261
380
  staleProgressHours: 6, // Hours before stuck indexing is considered stale
262
- safetyWindowMinutes: 10, // Minutes of recent activity to never delete
263
- removeDuplicates: true, // Remove duplicate workspace caches
264
- },
265
- watchFiles: true, // Enable file system watcher to re-index changed files in real-time
266
- verbose: false, // Enable detailed logging for debugging and progress tracking
267
- saveReaderWaitTimeoutMs: 5000, // Max wait for active reads before saving binary cache
268
- workerThreads: 'auto', // 0 = run in main thread (no workers), "auto" = CPU cores - 1, or set a number
269
- workerBatchTimeoutMs: 120000, // Timeout per worker batch before fallback (ms)
270
- workerFailureThreshold: 1, // Open circuit after N worker failures
271
- workerFailureCooldownMs: 10 * 60 * 1000, // Cooldown before retrying workers
272
- workerMaxChunksPerBatch: 100, // Cap chunks per worker batch to reduce hang risk
273
- allowSingleThreadFallback: false, // Allow fallback to main-thread embeddings if workers fail
274
- embeddingProcessPerBatch: false, // Use child process per batch for memory isolation
275
- autoEmbeddingProcessPerBatch: true, // Auto-enable child process embedding in single-threaded mode for heavy models
276
- embeddingBatchSize: null, // Override embedding batch size (null = auto)
277
- embeddingProcessNumThreads: 8, // ONNX threads used by embedding child process
278
- enableExplicitGc: true, // Require --expose-gc for more aggressive memory cleanup
279
- embeddingModel: 'jinaai/jina-embeddings-v2-base-code', // AI model ID used for semantic search - can be changed with a lighter model for speed
280
- embeddingDimension: null, // null = full dimensions, or 64/128/256/512/768 for MRL-trained models (e.g. nomic-embed-text-v1.5)
281
- preloadEmbeddingModel: true, // Preload the embedding model at startup (server mode)
282
- vectorStoreFormat: 'binary', // json | binary | sqlite (binary uses mmap-friendly on-disk store)
283
- vectorStoreContentMode: 'external', // external = content loaded on-demand for binary store
284
- contentCacheEntries: 256, // In-memory content cache entries for binary store
285
- vectorStoreLoadMode: 'memory', // memory | disk (disk streams vectors from disk / memory is faster but requires more RAM)
286
- vectorCacheEntries: 0, // In-memory vector cache entries for disk-backed loads
287
- clearCacheAfterIndex: true, // Drop in-memory vectors after indexing completes
288
- unloadModelAfterIndex: true, // Unload embedding model from memory after indexing completes to free RAM
289
- unloadModelAfterSearch: true, // Unload embedding model after search queries to keep memory low (trades speed for RAM)
290
- embeddingPoolIdleTimeoutMs: 10000, // Idle timeout before killing persistent embedding child process (ms)
291
- incrementalGcThresholdMb: 2048, // RSS threshold for optional incremental GC (requires enableExplicitGc)
292
- semanticWeight: 0.7, // Balance between semantic and keyword scores (0.0 to 1.0)
293
- exactMatchBoost: 1.5, // Multiplier applied when an exact string match is found
294
- recencyBoost: 0.1, // Boost for recently modified files (max 0.1 added to score)
295
- recencyDecayDays: 30, // After this many days, recency boost is 0
296
- textMatchMaxCandidates: 2000, // Max candidates for full text matching before deferring
297
- smartIndexing: true, // Enable automatic project type detection and smart ignore patterns
298
- callGraphEnabled: true, // Enable call graph extraction for proximity boosting
299
- callGraphBoost: 0.15, // Boost for files related via call graph (0-1)
300
- callGraphMaxHops: 1, // How many levels of calls to follow (1 = direct only)
301
- annEnabled: true, // Enable Approximate Nearest Neighbor (ANN) index for large codebases
302
- annMinChunks: 5000, // Minimum number of chunks required to trigger ANN indexing
303
- annMinCandidates: 50, // Minimum initial candidates to pull from ANN before refinement
304
- annMaxCandidates: 200, // Hard limit on the number of ANN candidates to process
305
- annCandidateMultiplier: 20, // Scale initial search depth based on requested maxResults
306
- annEfConstruction: 200, // HNSW index construction quality (higher = better index, slower build)
307
- annEfSearch: 64, // HNSW search parameter (higher = more accurate, slower search)
308
- annM: 16, // Number of connections per element in HNSW index
309
- annIndexCache: true, // Whether to cache the built HNSW index on disk
310
- annMetric: 'cosine', // Distance metric for similarity (currently locked to cosine)
311
- };
381
+ safetyWindowMinutes: 10, // Minutes of recent activity to never delete
382
+ removeDuplicates: true, // Remove duplicate workspace caches
383
+ },
384
+ watchFiles: DEFAULT_INDEXING_CONFIG.watchFiles,
385
+ verbose: DEFAULT_LOGGING_CONFIG.verbose,
386
+ memoryLogIntervalMs: DEFAULT_LOGGING_CONFIG.memoryLogIntervalMs,
387
+ saveReaderWaitTimeoutMs: DEFAULT_CACHE_CONFIG.saveReaderWaitTimeoutMs,
388
+ workerThreads: DEFAULT_WORKER_CONFIG.workerThreads,
389
+ workerBatchTimeoutMs: DEFAULT_WORKER_CONFIG.workerBatchTimeoutMs,
390
+ workerFailureThreshold: DEFAULT_WORKER_CONFIG.workerFailureThreshold,
391
+ workerFailureCooldownMs: DEFAULT_WORKER_CONFIG.workerFailureCooldownMs,
392
+ workerMaxChunksPerBatch: DEFAULT_WORKER_CONFIG.workerMaxChunksPerBatch,
393
+ allowSingleThreadFallback: DEFAULT_WORKER_CONFIG.allowSingleThreadFallback,
394
+ failFastEmbeddingErrors: DEFAULT_WORKER_CONFIG.failFastEmbeddingErrors,
395
+ embeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.embeddingProcessPerBatch,
396
+ autoEmbeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.autoEmbeddingProcessPerBatch,
397
+ embeddingBatchSize: DEFAULT_EMBEDDING_CONFIG.embeddingBatchSize,
398
+ embeddingProcessNumThreads: DEFAULT_EMBEDDING_CONFIG.embeddingProcessNumThreads,
399
+ embeddingProcessGcRssThresholdMb: DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcRssThresholdMb,
400
+ embeddingProcessGcMinIntervalMs: DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcMinIntervalMs,
401
+ embeddingProcessGcMaxRequestsWithoutCollection:
402
+ DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection,
403
+ enableExplicitGc: DEFAULT_MEMORY_CLEANUP_CONFIG.enableExplicitGc,
404
+ embeddingModel: DEFAULT_EMBEDDING_CONFIG.embeddingModel,
405
+ embeddingDimension: DEFAULT_EMBEDDING_CONFIG.embeddingDimension,
406
+ preloadEmbeddingModel: DEFAULT_EMBEDDING_CONFIG.preloadEmbeddingModel,
407
+ vectorStoreFormat: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreFormat,
408
+ vectorStoreContentMode: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreContentMode,
409
+ contentCacheEntries: DEFAULT_VECTOR_STORE_CONFIG.contentCacheEntries,
410
+ vectorStoreLoadMode: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreLoadMode,
411
+ vectorCacheEntries: DEFAULT_VECTOR_STORE_CONFIG.vectorCacheEntries,
412
+ clearCacheAfterIndex: DEFAULT_MEMORY_CLEANUP_CONFIG.clearCacheAfterIndex,
413
+ unloadModelAfterIndex: DEFAULT_MEMORY_CLEANUP_CONFIG.unloadModelAfterIndex,
414
+ shutdownQueryEmbeddingPoolAfterIndex:
415
+ DEFAULT_MEMORY_CLEANUP_CONFIG.shutdownQueryEmbeddingPoolAfterIndex,
416
+ unloadModelAfterSearch: DEFAULT_MEMORY_CLEANUP_CONFIG.unloadModelAfterSearch,
417
+ embeddingPoolIdleTimeoutMs: DEFAULT_MEMORY_CLEANUP_CONFIG.embeddingPoolIdleTimeoutMs,
418
+ incrementalGcThresholdMb: DEFAULT_MEMORY_CLEANUP_CONFIG.incrementalGcThresholdMb,
419
+ incrementalMemoryProfile: DEFAULT_MEMORY_CLEANUP_CONFIG.incrementalMemoryProfile,
420
+ recycleServerOnHighRssAfterIncremental:
421
+ DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssAfterIncremental,
422
+ recycleServerOnHighRssThresholdMb:
423
+ DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssThresholdMb,
424
+ recycleServerOnHighRssCooldownMs:
425
+ DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssCooldownMs,
426
+ recycleServerOnHighRssDelayMs: DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssDelayMs,
427
+ memoryCleanup: { ...DEFAULT_MEMORY_CLEANUP_CONFIG },
428
+ semanticWeight: DEFAULT_SEARCH_CONFIG.semanticWeight,
429
+ exactMatchBoost: DEFAULT_SEARCH_CONFIG.exactMatchBoost,
430
+ recencyBoost: DEFAULT_SEARCH_CONFIG.recencyBoost,
431
+ recencyDecayDays: DEFAULT_SEARCH_CONFIG.recencyDecayDays,
432
+ textMatchMaxCandidates: DEFAULT_SEARCH_CONFIG.textMatchMaxCandidates,
433
+ smartIndexing: DEFAULT_INDEXING_CONFIG.smartIndexing,
434
+ callGraphEnabled: DEFAULT_CALL_GRAPH_CONFIG.callGraphEnabled,
435
+ callGraphBoost: DEFAULT_CALL_GRAPH_CONFIG.callGraphBoost,
436
+ callGraphMaxHops: DEFAULT_CALL_GRAPH_CONFIG.callGraphMaxHops,
437
+ annEnabled: DEFAULT_ANN_CONFIG.annEnabled,
438
+ annMinChunks: DEFAULT_ANN_CONFIG.annMinChunks,
439
+ annMinCandidates: DEFAULT_ANN_CONFIG.annMinCandidates,
440
+ annMaxCandidates: DEFAULT_ANN_CONFIG.annMaxCandidates,
441
+ annCandidateMultiplier: DEFAULT_ANN_CONFIG.annCandidateMultiplier,
442
+ annEfConstruction: DEFAULT_ANN_CONFIG.annEfConstruction,
443
+ annEfSearch: DEFAULT_ANN_CONFIG.annEfSearch,
444
+ annM: DEFAULT_ANN_CONFIG.annM,
445
+ annIndexCache: DEFAULT_ANN_CONFIG.annIndexCache,
446
+ annMetric: DEFAULT_ANN_CONFIG.annMetric,
447
+ indexing: { ...DEFAULT_INDEXING_CONFIG },
448
+ logging: { ...DEFAULT_LOGGING_CONFIG },
449
+ cache: { ...DEFAULT_CACHE_CONFIG },
450
+ worker: { ...DEFAULT_WORKER_CONFIG },
451
+ embedding: { ...DEFAULT_EMBEDDING_CONFIG },
452
+ vectorStore: { ...DEFAULT_VECTOR_STORE_CONFIG },
453
+ search: { ...DEFAULT_SEARCH_CONFIG },
454
+ callGraph: { ...DEFAULT_CALL_GRAPH_CONFIG },
455
+ ann: { ...DEFAULT_ANN_CONFIG },
456
+ };
312
457
 
313
458
  let config = { ...DEFAULT_CONFIG };
314
459
 
@@ -323,7 +468,7 @@ const WORKSPACE_ENV_VARS = [
323
468
  'INIT_CWD',
324
469
  ];
325
470
 
326
- const WORKSPACE_MARKERS = [
471
+ const WORKSPACE_MARKERS = [
327
472
  '.git',
328
473
  'package.json',
329
474
  'pyproject.toml',
@@ -335,8 +480,118 @@ const WORKSPACE_MARKERS = [
335
480
  'requirements.txt',
336
481
  'Gemfile',
337
482
  'Makefile',
338
- 'CMakeLists.txt',
339
- ];
483
+ 'CMakeLists.txt',
484
+ ];
485
+
486
+ function hasOwn(obj, key) {
487
+ return Object.prototype.hasOwnProperty.call(obj, key);
488
+ }
489
+
490
+ const CONFIG_NAMESPACES = Object.freeze([
491
+ {
492
+ name: 'memoryCleanup',
493
+ keys: MEMORY_CLEANUP_KEYS,
494
+ defaults: DEFAULT_MEMORY_CLEANUP_CONFIG,
495
+ },
496
+ {
497
+ name: 'indexing',
498
+ keys: INDEXING_KEYS,
499
+ defaults: DEFAULT_INDEXING_CONFIG,
500
+ },
501
+ {
502
+ name: 'logging',
503
+ keys: LOGGING_KEYS,
504
+ defaults: DEFAULT_LOGGING_CONFIG,
505
+ },
506
+ {
507
+ name: 'cache',
508
+ keys: CACHE_KEYS,
509
+ defaults: DEFAULT_CACHE_CONFIG,
510
+ },
511
+ {
512
+ name: 'worker',
513
+ keys: WORKER_KEYS,
514
+ defaults: DEFAULT_WORKER_CONFIG,
515
+ },
516
+ {
517
+ name: 'embedding',
518
+ keys: EMBEDDING_KEYS,
519
+ defaults: DEFAULT_EMBEDDING_CONFIG,
520
+ },
521
+ {
522
+ name: 'vectorStore',
523
+ keys: VECTOR_STORE_KEYS,
524
+ defaults: DEFAULT_VECTOR_STORE_CONFIG,
525
+ },
526
+ {
527
+ name: 'search',
528
+ keys: SEARCH_KEYS,
529
+ defaults: DEFAULT_SEARCH_CONFIG,
530
+ },
531
+ {
532
+ name: 'callGraph',
533
+ keys: CALL_GRAPH_KEYS,
534
+ defaults: DEFAULT_CALL_GRAPH_CONFIG,
535
+ },
536
+ {
537
+ name: 'ann',
538
+ keys: ANN_KEYS,
539
+ defaults: DEFAULT_ANN_CONFIG,
540
+ },
541
+ ]);
542
+
543
+ function applyNamespace(targetConfig, sourceConfig, namespaceName, keys, defaults) {
544
+ const sourceNamespace =
545
+ sourceConfig && typeof sourceConfig[namespaceName] === 'object'
546
+ ? sourceConfig[namespaceName]
547
+ : {};
548
+ const mergedNamespace = {
549
+ ...defaults,
550
+ ...(targetConfig[namespaceName] && typeof targetConfig[namespaceName] === 'object'
551
+ ? targetConfig[namespaceName]
552
+ : {}),
553
+ };
554
+
555
+ for (const key of keys) {
556
+ if (hasOwn(sourceNamespace, key)) {
557
+ targetConfig[key] = mergedNamespace[key];
558
+ } else {
559
+ mergedNamespace[key] = targetConfig[key];
560
+ }
561
+ }
562
+
563
+ targetConfig[namespaceName] = mergedNamespace;
564
+ }
565
+
566
+ function syncNamespace(targetConfig, namespaceName, keys, defaults) {
567
+ const currentNamespace =
568
+ targetConfig[namespaceName] && typeof targetConfig[namespaceName] === 'object'
569
+ ? targetConfig[namespaceName]
570
+ : {};
571
+ const mergedNamespace = { ...defaults, ...currentNamespace };
572
+ for (const key of keys) {
573
+ mergedNamespace[key] = targetConfig[key];
574
+ }
575
+ targetConfig[namespaceName] = mergedNamespace;
576
+ }
577
+
578
+ function applyAllNamespaces(targetConfig, sourceConfig) {
579
+ for (const namespace of CONFIG_NAMESPACES) {
580
+ applyNamespace(
581
+ targetConfig,
582
+ sourceConfig,
583
+ namespace.name,
584
+ namespace.keys,
585
+ namespace.defaults
586
+ );
587
+ }
588
+ }
589
+
590
+ function syncAllNamespaces(targetConfig) {
591
+ for (const namespace of CONFIG_NAMESPACES) {
592
+ syncNamespace(targetConfig, namespace.name, namespace.keys, namespace.defaults);
593
+ }
594
+ }
340
595
 
341
596
  async function pathExists(filePath) {
342
597
  try {
@@ -444,7 +699,16 @@ export async function loadConfig(workspaceDir = null) {
444
699
  }
445
700
  }
446
701
 
447
- config = { ...DEFAULT_CONFIG, ...userConfig };
702
+ config = { ...DEFAULT_CONFIG, ...userConfig };
703
+ applyAllNamespaces(config, userConfig);
704
+
705
+ // Backward compatibility for legacy top-level cache cleanup toggle.
706
+ if (
707
+ hasOwn(userConfig, 'autoCleanStaleCaches') &&
708
+ !(userConfig.cacheCleanup && hasOwn(userConfig.cacheCleanup, 'autoCleanup'))
709
+ ) {
710
+ config.cacheCleanup.autoCleanup = Boolean(userConfig.autoCleanStaleCaches);
711
+ }
448
712
 
449
713
  // Set search directory (respect user override when provided)
450
714
  if (userConfig.searchDirectory) {
@@ -479,7 +743,7 @@ export async function loadConfig(workspaceDir = null) {
479
743
  if (stats.isDirectory()) {
480
744
  config.cacheDirectory = legacyPath;
481
745
  if (config.verbose) {
482
- console.error(`[Config] Using existing local cache: ${legacyPath}`);
746
+ console.info(`[Config] Using existing local cache: ${legacyPath}`);
483
747
  }
484
748
  }
485
749
  } catch {
@@ -524,6 +788,17 @@ export async function loadConfig(workspaceDir = null) {
524
788
  }
525
789
  }
526
790
 
791
+ if (process.env.SMART_CODING_MEMORY_LOG_INTERVAL_MS !== undefined) {
792
+ const value = parseInt(process.env.SMART_CODING_MEMORY_LOG_INTERVAL_MS, 10);
793
+ if (!isNaN(value) && value >= 0 && value <= 300000) {
794
+ config.memoryLogIntervalMs = value;
795
+ } else {
796
+ console.warn(
797
+ `[Config] Invalid SMART_CODING_MEMORY_LOG_INTERVAL_MS: ${process.env.SMART_CODING_MEMORY_LOG_INTERVAL_MS}, using default`
798
+ );
799
+ }
800
+ }
801
+
527
802
  if (process.env.SMART_CODING_BATCH_SIZE !== undefined) {
528
803
  const value = parseInt(process.env.SMART_CODING_BATCH_SIZE, 10);
529
804
  if (!isNaN(value) && value > 0 && value <= 1000) {
@@ -697,6 +972,17 @@ export async function loadConfig(workspaceDir = null) {
697
972
  }
698
973
  }
699
974
 
975
+ if (process.env.SMART_CODING_SHUTDOWN_QUERY_POOL_AFTER_INDEX !== undefined) {
976
+ const value = process.env.SMART_CODING_SHUTDOWN_QUERY_POOL_AFTER_INDEX;
977
+ if (value === 'true' || value === 'false') {
978
+ config.shutdownQueryEmbeddingPoolAfterIndex = value === 'true';
979
+ } else {
980
+ console.warn(
981
+ `[Config] Invalid SMART_CODING_SHUTDOWN_QUERY_POOL_AFTER_INDEX: ${value}, using default`
982
+ );
983
+ }
984
+ }
985
+
700
986
  if (process.env.SMART_CODING_UNLOAD_MODEL_AFTER_SEARCH !== undefined) {
701
987
  const value = process.env.SMART_CODING_UNLOAD_MODEL_AFTER_SEARCH;
702
988
  if (value === 'true' || value === 'false') {
@@ -704,19 +990,74 @@ export async function loadConfig(workspaceDir = null) {
704
990
  }
705
991
  }
706
992
 
707
- if (process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB !== undefined) {
708
- const value = parseInt(process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB, 10);
709
- if (!isNaN(value) && value >= 0) {
710
- config.incrementalGcThresholdMb = value;
993
+ if (process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB !== undefined) {
994
+ const value = parseInt(process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB, 10);
995
+ if (!isNaN(value) && value >= 0) {
996
+ config.incrementalGcThresholdMb = value;
711
997
  } else {
712
998
  console.warn(
713
999
  `[Config] Invalid SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB: ${process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB}, using default`
714
- );
715
- }
716
- }
717
-
718
- if (process.env.SMART_CODING_CONTENT_CACHE_ENTRIES !== undefined) {
719
- const value = parseInt(process.env.SMART_CODING_CONTENT_CACHE_ENTRIES, 10);
1000
+ );
1001
+ }
1002
+ }
1003
+
1004
+ if (process.env.SMART_CODING_INCREMENTAL_MEMORY_PROFILE !== undefined) {
1005
+ const value = process.env.SMART_CODING_INCREMENTAL_MEMORY_PROFILE;
1006
+ if (value === 'true' || value === 'false') {
1007
+ config.incrementalMemoryProfile = value === 'true';
1008
+ } else {
1009
+ console.warn(
1010
+ `[Config] Invalid SMART_CODING_INCREMENTAL_MEMORY_PROFILE: ${value}, using default`
1011
+ );
1012
+ }
1013
+ }
1014
+
1015
+ if (process.env.SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL !== undefined) {
1016
+ const value = process.env.SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL;
1017
+ if (value === 'true' || value === 'false') {
1018
+ config.recycleServerOnHighRssAfterIncremental = value === 'true';
1019
+ } else {
1020
+ console.warn(
1021
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL: ${value}, using default`
1022
+ );
1023
+ }
1024
+ }
1025
+
1026
+ if (process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB !== undefined) {
1027
+ const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB, 10);
1028
+ if (!isNaN(value) && value > 0) {
1029
+ config.recycleServerOnHighRssThresholdMb = value;
1030
+ } else {
1031
+ console.warn(
1032
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB: ${process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB}, using default`
1033
+ );
1034
+ }
1035
+ }
1036
+
1037
+ if (process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS !== undefined) {
1038
+ const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS, 10);
1039
+ if (!isNaN(value) && value >= 0) {
1040
+ config.recycleServerOnHighRssCooldownMs = value;
1041
+ } else {
1042
+ console.warn(
1043
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS: ${process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS}, using default`
1044
+ );
1045
+ }
1046
+ }
1047
+
1048
+ if (process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS !== undefined) {
1049
+ const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS, 10);
1050
+ if (!isNaN(value) && value >= 0) {
1051
+ config.recycleServerOnHighRssDelayMs = value;
1052
+ } else {
1053
+ console.warn(
1054
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_DELAY_MS: ${process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS}, using default`
1055
+ );
1056
+ }
1057
+ }
1058
+
1059
+ if (process.env.SMART_CODING_CONTENT_CACHE_ENTRIES !== undefined) {
1060
+ const value = parseInt(process.env.SMART_CODING_CONTENT_CACHE_ENTRIES, 10);
720
1061
  if (!isNaN(value) && value >= 0 && value <= 10000) {
721
1062
  config.contentCacheEntries = value;
722
1063
  } else {
@@ -753,6 +1094,17 @@ export async function loadConfig(workspaceDir = null) {
753
1094
  }
754
1095
  }
755
1096
 
1097
+ if (process.env.SMART_CODING_EMBEDDING_FAIL_FAST_BREAKER !== undefined) {
1098
+ const value = process.env.SMART_CODING_EMBEDDING_FAIL_FAST_BREAKER;
1099
+ if (value === 'true' || value === 'false') {
1100
+ config.failFastEmbeddingErrors = value === 'true';
1101
+ } else {
1102
+ console.warn(
1103
+ `[Config] Invalid SMART_CODING_EMBEDDING_FAIL_FAST_BREAKER: ${value}, using default`
1104
+ );
1105
+ }
1106
+ }
1107
+
756
1108
  if (process.env.SMART_CODING_EMBEDDING_BATCH_SIZE !== undefined) {
757
1109
  const value = parseInt(process.env.SMART_CODING_EMBEDDING_BATCH_SIZE, 10);
758
1110
  if (!isNaN(value) && value > 0 && value <= 256) {
@@ -775,6 +1127,42 @@ export async function loadConfig(workspaceDir = null) {
775
1127
  }
776
1128
  }
777
1129
 
1130
+ if (process.env.SMART_CODING_EMBEDDING_PROCESS_GC_RSS_THRESHOLD_MB !== undefined) {
1131
+ const value = parseInt(process.env.SMART_CODING_EMBEDDING_PROCESS_GC_RSS_THRESHOLD_MB, 10);
1132
+ if (!isNaN(value) && value > 0) {
1133
+ config.embeddingProcessGcRssThresholdMb = value;
1134
+ } else {
1135
+ console.warn(
1136
+ `[Config] Invalid SMART_CODING_EMBEDDING_PROCESS_GC_RSS_THRESHOLD_MB: ${process.env.SMART_CODING_EMBEDDING_PROCESS_GC_RSS_THRESHOLD_MB}, using default`
1137
+ );
1138
+ }
1139
+ }
1140
+
1141
+ if (process.env.SMART_CODING_EMBEDDING_PROCESS_GC_MIN_INTERVAL_MS !== undefined) {
1142
+ const value = parseInt(process.env.SMART_CODING_EMBEDDING_PROCESS_GC_MIN_INTERVAL_MS, 10);
1143
+ if (!isNaN(value) && value >= 0) {
1144
+ config.embeddingProcessGcMinIntervalMs = value;
1145
+ } else {
1146
+ console.warn(
1147
+ `[Config] Invalid SMART_CODING_EMBEDDING_PROCESS_GC_MIN_INTERVAL_MS: ${process.env.SMART_CODING_EMBEDDING_PROCESS_GC_MIN_INTERVAL_MS}, using default`
1148
+ );
1149
+ }
1150
+ }
1151
+
1152
+ const embeddingProcessGcMaxRequestsEnv =
1153
+ process.env.SMART_CODING_EMBEDDING_PROCESS_GC_MAX_REQUESTS ??
1154
+ process.env.SMART_CODING_EMBEDDING_PROCESS_GC_MAX_REQUESTS_WITHOUT_COLLECTION;
1155
+ if (embeddingProcessGcMaxRequestsEnv !== undefined) {
1156
+ const value = parseInt(embeddingProcessGcMaxRequestsEnv, 10);
1157
+ if (!isNaN(value) && value > 0) {
1158
+ config.embeddingProcessGcMaxRequestsWithoutCollection = value;
1159
+ } else {
1160
+ console.warn(
1161
+ `[Config] Invalid SMART_CODING_EMBEDDING_PROCESS_GC_MAX_REQUESTS: ${embeddingProcessGcMaxRequestsEnv}, using default`
1162
+ );
1163
+ }
1164
+ }
1165
+
778
1166
  if (process.env.SMART_CODING_ANN_ENABLED !== undefined) {
779
1167
  const value = process.env.SMART_CODING_ANN_ENABLED;
780
1168
  if (value === 'true' || value === 'false') {
@@ -922,8 +1310,69 @@ export async function loadConfig(workspaceDir = null) {
922
1310
  }
923
1311
  }
924
1312
 
925
- return config;
926
- }
1313
+ if (config.memoryLogIntervalMs !== null && config.memoryLogIntervalMs !== undefined) {
1314
+ const value = parseInt(config.memoryLogIntervalMs, 10);
1315
+ if (!isNaN(value) && value >= 0 && value <= 300000) {
1316
+ config.memoryLogIntervalMs = value;
1317
+ } else {
1318
+ console.warn(
1319
+ `[Config] Invalid memoryLogIntervalMs: ${config.memoryLogIntervalMs}, using default`
1320
+ );
1321
+ config.memoryLogIntervalMs = DEFAULT_CONFIG.memoryLogIntervalMs;
1322
+ }
1323
+ }
1324
+
1325
+ if (
1326
+ config.embeddingProcessGcRssThresholdMb !== null &&
1327
+ config.embeddingProcessGcRssThresholdMb !== undefined
1328
+ ) {
1329
+ const value = parseInt(config.embeddingProcessGcRssThresholdMb, 10);
1330
+ if (!isNaN(value) && value > 0) {
1331
+ config.embeddingProcessGcRssThresholdMb = value;
1332
+ } else {
1333
+ console.warn(
1334
+ `[Config] Invalid embeddingProcessGcRssThresholdMb: ${config.embeddingProcessGcRssThresholdMb}, using default`
1335
+ );
1336
+ config.embeddingProcessGcRssThresholdMb =
1337
+ DEFAULT_CONFIG.embeddingProcessGcRssThresholdMb;
1338
+ }
1339
+ }
1340
+
1341
+ if (
1342
+ config.embeddingProcessGcMinIntervalMs !== null &&
1343
+ config.embeddingProcessGcMinIntervalMs !== undefined
1344
+ ) {
1345
+ const value = parseInt(config.embeddingProcessGcMinIntervalMs, 10);
1346
+ if (!isNaN(value) && value >= 0) {
1347
+ config.embeddingProcessGcMinIntervalMs = value;
1348
+ } else {
1349
+ console.warn(
1350
+ `[Config] Invalid embeddingProcessGcMinIntervalMs: ${config.embeddingProcessGcMinIntervalMs}, using default`
1351
+ );
1352
+ config.embeddingProcessGcMinIntervalMs =
1353
+ DEFAULT_CONFIG.embeddingProcessGcMinIntervalMs;
1354
+ }
1355
+ }
1356
+
1357
+ if (
1358
+ config.embeddingProcessGcMaxRequestsWithoutCollection !== null &&
1359
+ config.embeddingProcessGcMaxRequestsWithoutCollection !== undefined
1360
+ ) {
1361
+ const value = parseInt(config.embeddingProcessGcMaxRequestsWithoutCollection, 10);
1362
+ if (!isNaN(value) && value > 0) {
1363
+ config.embeddingProcessGcMaxRequestsWithoutCollection = value;
1364
+ } else {
1365
+ console.warn(
1366
+ `[Config] Invalid embeddingProcessGcMaxRequestsWithoutCollection: ${config.embeddingProcessGcMaxRequestsWithoutCollection}, using default`
1367
+ );
1368
+ config.embeddingProcessGcMaxRequestsWithoutCollection =
1369
+ DEFAULT_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection;
1370
+ }
1371
+ }
1372
+
1373
+ syncAllNamespaces(config);
1374
+ return config;
1375
+ }
927
1376
 
928
1377
  /**
929
1378
  * Get platform-specific global cache directory