@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
package/lib/config.js CHANGED
@@ -3,130 +3,130 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { fileURLToPath } from 'url';
6
- import { ProjectDetector } from './project-detector.js';
7
- import { parseJsonc } from './settings-editor.js';
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
- import { getWorkspaceEnvKeys } from './workspace-env.js';
14
-
15
- const DEFAULT_MEMORY_CLEANUP_CONFIG = {
16
- enableExplicitGc: true, // Require --expose-gc for more aggressive memory cleanup
17
- clearCacheAfterIndex: true, // Drop in-memory vectors after indexing completes
18
- unloadModelAfterIndex: true, // Unload embedding model from memory after indexing completes to free RAM
19
- shutdownQueryEmbeddingPoolAfterIndex: true, // Force shutdown search embedding child pool after index operations
20
- unloadModelAfterSearch: true, // Unload embedding model after search queries to keep memory low (trades speed for RAM)
21
- embeddingPoolIdleTimeoutMs: 2000, // Idle timeout before killing persistent embedding child process (ms)
22
- incrementalGcThresholdMb: 512, // RSS threshold for optional incremental GC
23
- incrementalMemoryProfile: false, // Enable phase-level incremental indexing memory traces (diagnostics)
24
- recycleServerOnHighRssAfterIncremental: false, // Recycle server process after incremental cleanup if RSS remains high
25
- recycleServerOnHighRssThresholdMb: 4096, // RSS threshold (MB) that triggers incremental recycle
26
- recycleServerOnHighRssCooldownMs: 300000, // Minimum interval between recycle attempts
27
- recycleServerOnHighRssDelayMs: 2000, // Delay before recycle to allow logs/responses to flush
28
- };
29
-
30
- const DEFAULT_INDEXING_CONFIG = {
31
- smartIndexing: true, // Enable automatic project type detection and smart ignore patterns
32
- chunkSize: 16, // Lines per chunk (tuned for speed/memory balance)
33
- chunkOverlap: 4, // Overlap between chunks for context continuity
34
- batchSize: 50, // Number of files to process in a single indexing batch
35
- maxFileSize: 1048576, // 1MB - skip files larger than this
36
- prefilterContentMaxBytes: 512 * 1024, // 512KB - cache content during prefilter to avoid double reads
37
- maxResults: 5, // Maximum number of semantic search results to return
38
- watchFiles: true, // Enable file system watcher to re-index changed files in real-time
39
- };
40
-
41
- const DEFAULT_LOGGING_CONFIG = {
42
- verbose: false, // Enable detailed logging for debugging and progress tracking
43
- memoryLogIntervalMs: 5000, // Verbose memory log cadence during indexing (ms)
44
- };
45
-
46
- const DEFAULT_CACHE_CONFIG = {
47
- enableCache: true, // Whether to persist and reload embeddings between sessions
48
- saveReaderWaitTimeoutMs: 5000, // Max wait for active reads before saving binary cache
49
- cacheVectorAssumeFinite: true, // Assume vectors are finite (skip validation)
50
- cacheVectorFloatDigits: null, // Decimal precision for cached vectors (null = default)
51
- cacheWriteHighWaterMark: 262144, // Write stream highWaterMark for cache files
52
- cacheVectorFlushChars: 262144, // Flush threshold (chars) for JSON writer
53
- cacheVectorCheckFinite: true, // Validate vectors contain only finite numbers
54
- cacheVectorNoMutation: false, // Avoid mutating vectors during serialization
55
- cacheVectorJoinThreshold: 8192, // Join threshold for JSON array chunks
56
- cacheVectorJoinChunkSize: 2048, // Chunk size for JSON join optimization
57
- };
58
-
59
- const DEFAULT_WORKER_CONFIG = {
60
- workerThreads: 'auto', // 0 = run in main thread (no workers), "auto" = CPU cores - 1, or set a number
61
- workerBatchTimeoutMs: 120000, // Timeout per worker batch before fallback (ms)
62
- workerFailureThreshold: 1, // Open circuit after N worker failures
63
- workerFailureCooldownMs: 10 * 60 * 1000, // Cooldown before retrying workers
64
- workerMaxChunksPerBatch: 100, // Cap chunks per worker batch to reduce hang risk
65
- allowSingleThreadFallback: false, // Allow fallback to main-thread embeddings if workers fail
66
- failFastEmbeddingErrors: false, // Abort worker embedding batch after repeated consecutive embed failures
67
- };
68
-
69
- const DEFAULT_EMBEDDING_CONFIG = {
70
- embeddingModel: 'jinaai/jina-embeddings-v2-base-code', // AI model ID used for semantic search
71
- embeddingDimension: null, // null = full dimensions, or 64/128/256/512/768 for MRL-trained models
72
- preloadEmbeddingModel: true, // Preload the embedding model at startup (server mode)
73
- embeddingProcessPerBatch: false, // Use child process per batch for memory isolation
74
- autoEmbeddingProcessPerBatch: true, // Auto-enable child process embedding in single-threaded mode for heavy models
75
- embeddingBatchSize: null, // Override embedding batch size (null = auto)
76
- embeddingProcessNumThreads: 8, // ONNX threads used by embedding child process
77
- embeddingProcessGcRssThresholdMb: EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB, // RSS threshold for embedding-child adaptive GC
78
- embeddingProcessGcMinIntervalMs: EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS, // Minimum interval between embedding-child GC runs
79
- embeddingProcessGcMaxRequestsWithoutCollection:
80
- EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION, // Backstop GC cadence for embedding child
81
- };
82
-
83
- const DEFAULT_VECTOR_STORE_CONFIG = {
84
- vectorStoreFormat: 'binary', // json | binary | sqlite (binary uses mmap-friendly on-disk store)
85
- vectorStoreContentMode: 'external', // external = content loaded on-demand for binary store
86
- contentCacheEntries: 256, // In-memory content cache entries for binary store
87
- vectorStoreLoadMode: 'memory', // memory | disk (disk streams vectors from disk / memory is faster but requires more RAM)
88
- vectorCacheEntries: 0, // In-memory vector cache entries for disk-backed loads
89
- };
90
-
91
- const DEFAULT_SEARCH_CONFIG = {
92
- semanticWeight: 0.7, // Balance between semantic and keyword scores (0.0 to 1.0)
93
- exactMatchBoost: 1.5, // Multiplier applied when an exact string match is found
94
- recencyBoost: 0.1, // Boost for recently modified files (max 0.1 added to score)
95
- recencyDecayDays: 30, // After this many days, recency boost is 0
96
- textMatchMaxCandidates: 2000, // Max candidates for full text matching before deferring
97
- };
98
-
99
- const DEFAULT_CALL_GRAPH_CONFIG = {
100
- callGraphEnabled: true, // Enable call graph extraction for proximity boosting
101
- callGraphBoost: 0.15, // Boost for files related via call graph (0-1)
102
- callGraphMaxHops: 1, // How many levels of calls to follow (1 = direct only)
103
- };
104
-
105
- const DEFAULT_ANN_CONFIG = {
106
- annEnabled: true, // Enable Approximate Nearest Neighbor (ANN) index for large codebases
107
- annMinChunks: 5000, // Minimum number of chunks required to trigger ANN indexing
108
- annMinCandidates: 50, // Minimum initial candidates to pull from ANN before refinement
109
- annMaxCandidates: 200, // Hard limit on the number of ANN candidates to process
110
- annCandidateMultiplier: 20, // Scale initial search depth based on requested maxResults
111
- annEfConstruction: 200, // HNSW index construction quality (higher = better index, slower build)
112
- annEfSearch: 64, // HNSW search parameter (higher = more accurate, slower search)
113
- annM: 16, // Number of connections per element in HNSW index
114
- annIndexCache: true, // Whether to cache the built HNSW index on disk
115
- annMetric: 'cosine', // Distance metric for similarity (currently locked to cosine)
116
- };
117
-
118
- const MEMORY_CLEANUP_KEYS = Object.freeze(Object.keys(DEFAULT_MEMORY_CLEANUP_CONFIG));
119
- const INDEXING_KEYS = Object.freeze(Object.keys(DEFAULT_INDEXING_CONFIG));
120
- const LOGGING_KEYS = Object.freeze(Object.keys(DEFAULT_LOGGING_CONFIG));
121
- const CACHE_KEYS = Object.freeze(Object.keys(DEFAULT_CACHE_CONFIG));
122
- const WORKER_KEYS = Object.freeze(Object.keys(DEFAULT_WORKER_CONFIG));
123
- const EMBEDDING_KEYS = Object.freeze(Object.keys(DEFAULT_EMBEDDING_CONFIG));
124
- const VECTOR_STORE_KEYS = Object.freeze(Object.keys(DEFAULT_VECTOR_STORE_CONFIG));
125
- const SEARCH_KEYS = Object.freeze(Object.keys(DEFAULT_SEARCH_CONFIG));
126
- const CALL_GRAPH_KEYS = Object.freeze(Object.keys(DEFAULT_CALL_GRAPH_CONFIG));
127
- const ANN_KEYS = Object.freeze(Object.keys(DEFAULT_ANN_CONFIG));
128
-
129
- const DEFAULT_CONFIG = {
6
+ import { ProjectDetector } from './project-detector.js';
7
+ import { parseJsonc } from './settings-editor.js';
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
+ import { getWorkspaceEnvKeys } from './workspace-env.js';
14
+
15
+ const DEFAULT_MEMORY_CLEANUP_CONFIG = {
16
+ enableExplicitGc: true, // Require --expose-gc for more aggressive memory cleanup
17
+ clearCacheAfterIndex: true, // Drop in-memory vectors after indexing completes
18
+ unloadModelAfterIndex: true, // Unload embedding model from memory after indexing completes to free RAM
19
+ shutdownQueryEmbeddingPoolAfterIndex: true, // Force shutdown search embedding child pool after index operations
20
+ unloadModelAfterSearch: true, // Unload embedding model after search queries to keep memory low (trades speed for RAM)
21
+ embeddingPoolIdleTimeoutMs: 2000, // Idle timeout before killing persistent embedding child process (ms)
22
+ incrementalGcThresholdMb: 512, // RSS threshold for optional incremental GC
23
+ incrementalMemoryProfile: false, // Enable phase-level incremental indexing memory traces (diagnostics)
24
+ recycleServerOnHighRssAfterIncremental: false, // Recycle server process after incremental cleanup if RSS remains high
25
+ recycleServerOnHighRssThresholdMb: 4096, // RSS threshold (MB) that triggers incremental recycle
26
+ recycleServerOnHighRssCooldownMs: 300000, // Minimum interval between recycle attempts
27
+ recycleServerOnHighRssDelayMs: 2000, // Delay before recycle to allow logs/responses to flush
28
+ };
29
+
30
+ const DEFAULT_INDEXING_CONFIG = {
31
+ smartIndexing: true, // Enable automatic project type detection and smart ignore patterns
32
+ chunkSize: 16, // Lines per chunk (tuned for speed/memory balance)
33
+ chunkOverlap: 4, // Overlap between chunks for context continuity
34
+ batchSize: 50, // Number of files to process in a single indexing batch
35
+ maxFileSize: 1048576, // 1MB - skip files larger than this
36
+ prefilterContentMaxBytes: 512 * 1024, // 512KB - cache content during prefilter to avoid double reads
37
+ maxResults: 5, // Maximum number of semantic search results to return
38
+ watchFiles: true, // Enable file system watcher to re-index changed files in real-time
39
+ };
40
+
41
+ const DEFAULT_LOGGING_CONFIG = {
42
+ verbose: false, // Enable detailed logging for debugging and progress tracking
43
+ memoryLogIntervalMs: 5000, // Verbose memory log cadence during indexing (ms)
44
+ };
45
+
46
+ const DEFAULT_CACHE_CONFIG = {
47
+ enableCache: true, // Whether to persist and reload embeddings between sessions
48
+ saveReaderWaitTimeoutMs: 5000, // Max wait for active reads before saving binary cache
49
+ cacheVectorAssumeFinite: true, // Assume vectors are finite (skip validation)
50
+ cacheVectorFloatDigits: null, // Decimal precision for cached vectors (null = default)
51
+ cacheWriteHighWaterMark: 262144, // Write stream highWaterMark for cache files
52
+ cacheVectorFlushChars: 262144, // Flush threshold (chars) for JSON writer
53
+ cacheVectorCheckFinite: true, // Validate vectors contain only finite numbers
54
+ cacheVectorNoMutation: false, // Avoid mutating vectors during serialization
55
+ cacheVectorJoinThreshold: 8192, // Join threshold for JSON array chunks
56
+ cacheVectorJoinChunkSize: 2048, // Chunk size for JSON join optimization
57
+ };
58
+
59
+ const DEFAULT_WORKER_CONFIG = {
60
+ workerThreads: 'auto', // 0 = run in main thread (no workers), "auto" = CPU cores - 1, or set a number
61
+ workerBatchTimeoutMs: 120000, // Timeout per worker batch before fallback (ms)
62
+ workerFailureThreshold: 1, // Open circuit after N worker failures
63
+ workerFailureCooldownMs: 10 * 60 * 1000, // Cooldown before retrying workers
64
+ workerMaxChunksPerBatch: 100, // Cap chunks per worker batch to reduce hang risk
65
+ allowSingleThreadFallback: false, // Allow fallback to main-thread embeddings if workers fail
66
+ failFastEmbeddingErrors: false, // Abort worker embedding batch after repeated consecutive embed failures
67
+ };
68
+
69
+ const DEFAULT_EMBEDDING_CONFIG = {
70
+ embeddingModel: 'jinaai/jina-embeddings-v2-base-code', // AI model ID used for semantic search
71
+ embeddingDimension: null, // null = full dimensions, or 64/128/256/512/768 for MRL-trained models
72
+ preloadEmbeddingModel: true, // Preload the embedding model at startup (server mode)
73
+ embeddingProcessPerBatch: false, // Use child process per batch for memory isolation
74
+ autoEmbeddingProcessPerBatch: true, // Auto-enable child process embedding in single-threaded mode for heavy models
75
+ embeddingBatchSize: null, // Override embedding batch size (null = auto)
76
+ embeddingProcessNumThreads: 8, // ONNX threads used by embedding child process
77
+ embeddingProcessGcRssThresholdMb: EMBEDDING_PROCESS_DEFAULT_GC_RSS_THRESHOLD_MB, // RSS threshold for embedding-child adaptive GC
78
+ embeddingProcessGcMinIntervalMs: EMBEDDING_PROCESS_DEFAULT_GC_MIN_INTERVAL_MS, // Minimum interval between embedding-child GC runs
79
+ embeddingProcessGcMaxRequestsWithoutCollection:
80
+ EMBEDDING_PROCESS_DEFAULT_GC_MAX_REQUESTS_WITHOUT_COLLECTION, // Backstop GC cadence for embedding child
81
+ };
82
+
83
+ const DEFAULT_VECTOR_STORE_CONFIG = {
84
+ vectorStoreFormat: 'binary', // json | binary | sqlite (binary uses mmap-friendly on-disk store)
85
+ vectorStoreContentMode: 'external', // external = content loaded on-demand for binary store
86
+ contentCacheEntries: 256, // In-memory content cache entries for binary store
87
+ vectorStoreLoadMode: 'memory', // memory | disk (disk streams vectors from disk / memory is faster but requires more RAM)
88
+ vectorCacheEntries: 0, // In-memory vector cache entries for disk-backed loads
89
+ };
90
+
91
+ const DEFAULT_SEARCH_CONFIG = {
92
+ semanticWeight: 0.7, // Balance between semantic and keyword scores (0.0 to 1.0)
93
+ exactMatchBoost: 1.5, // Multiplier applied when an exact string match is found
94
+ recencyBoost: 0.1, // Boost for recently modified files (max 0.1 added to score)
95
+ recencyDecayDays: 30, // After this many days, recency boost is 0
96
+ textMatchMaxCandidates: 2000, // Max candidates for full text matching before deferring
97
+ };
98
+
99
+ const DEFAULT_CALL_GRAPH_CONFIG = {
100
+ callGraphEnabled: true, // Enable call graph extraction for proximity boosting
101
+ callGraphBoost: 0.15, // Boost for files related via call graph (0-1)
102
+ callGraphMaxHops: 1, // How many levels of calls to follow (1 = direct only)
103
+ };
104
+
105
+ const DEFAULT_ANN_CONFIG = {
106
+ annEnabled: true, // Enable Approximate Nearest Neighbor (ANN) index for large codebases
107
+ annMinChunks: 5000, // Minimum number of chunks required to trigger ANN indexing
108
+ annMinCandidates: 50, // Minimum initial candidates to pull from ANN before refinement
109
+ annMaxCandidates: 200, // Hard limit on the number of ANN candidates to process
110
+ annCandidateMultiplier: 20, // Scale initial search depth based on requested maxResults
111
+ annEfConstruction: 200, // HNSW index construction quality (higher = better index, slower build)
112
+ annEfSearch: 64, // HNSW search parameter (higher = more accurate, slower search)
113
+ annM: 16, // Number of connections per element in HNSW index
114
+ annIndexCache: true, // Whether to cache the built HNSW index on disk
115
+ annMetric: 'cosine', // Distance metric for similarity (currently locked to cosine)
116
+ };
117
+
118
+ const MEMORY_CLEANUP_KEYS = Object.freeze(Object.keys(DEFAULT_MEMORY_CLEANUP_CONFIG));
119
+ const INDEXING_KEYS = Object.freeze(Object.keys(DEFAULT_INDEXING_CONFIG));
120
+ const LOGGING_KEYS = Object.freeze(Object.keys(DEFAULT_LOGGING_CONFIG));
121
+ const CACHE_KEYS = Object.freeze(Object.keys(DEFAULT_CACHE_CONFIG));
122
+ const WORKER_KEYS = Object.freeze(Object.keys(DEFAULT_WORKER_CONFIG));
123
+ const EMBEDDING_KEYS = Object.freeze(Object.keys(DEFAULT_EMBEDDING_CONFIG));
124
+ const VECTOR_STORE_KEYS = Object.freeze(Object.keys(DEFAULT_VECTOR_STORE_CONFIG));
125
+ const SEARCH_KEYS = Object.freeze(Object.keys(DEFAULT_SEARCH_CONFIG));
126
+ const CALL_GRAPH_KEYS = Object.freeze(Object.keys(DEFAULT_CALL_GRAPH_CONFIG));
127
+ const ANN_KEYS = Object.freeze(Object.keys(DEFAULT_ANN_CONFIG));
128
+
129
+ const DEFAULT_CONFIG = {
130
130
  searchDirectory: '.',
131
131
  fileExtensions: [
132
132
  // JavaScript/TypeScript
@@ -362,16 +362,16 @@ const DEFAULT_CONFIG = {
362
362
  '**/scripts/**',
363
363
  '**/tools/**',
364
364
  ],
365
- chunkSize: DEFAULT_INDEXING_CONFIG.chunkSize,
366
- chunkOverlap: DEFAULT_INDEXING_CONFIG.chunkOverlap,
367
- batchSize: DEFAULT_INDEXING_CONFIG.batchSize,
368
- maxFileSize: DEFAULT_INDEXING_CONFIG.maxFileSize,
369
- prefilterContentMaxBytes: DEFAULT_INDEXING_CONFIG.prefilterContentMaxBytes,
370
- maxResults: DEFAULT_INDEXING_CONFIG.maxResults,
371
- enableCache: DEFAULT_CACHE_CONFIG.enableCache,
372
- cacheDirectory: null, // Will be set dynamically by loadConfig()
373
- // Cache cleanup behavior (consolidated namespace)
374
- cacheCleanup: {
365
+ chunkSize: DEFAULT_INDEXING_CONFIG.chunkSize,
366
+ chunkOverlap: DEFAULT_INDEXING_CONFIG.chunkOverlap,
367
+ batchSize: DEFAULT_INDEXING_CONFIG.batchSize,
368
+ maxFileSize: DEFAULT_INDEXING_CONFIG.maxFileSize,
369
+ prefilterContentMaxBytes: DEFAULT_INDEXING_CONFIG.prefilterContentMaxBytes,
370
+ maxResults: DEFAULT_INDEXING_CONFIG.maxResults,
371
+ enableCache: DEFAULT_CACHE_CONFIG.enableCache,
372
+ cacheDirectory: null, // Will be set dynamically by loadConfig()
373
+ // Cache cleanup behavior (consolidated namespace)
374
+ cacheCleanup: {
375
375
  autoCleanup: true, // Automatically remove stale caches on startup
376
376
  staleNoMetaHours: 6, // Hours before incomplete cache (no meta.json) is considered stale
377
377
  emptyThresholdHours: 24, // Hours before empty cache (0 files/chunks) is removed
@@ -379,86 +379,86 @@ const DEFAULT_CONFIG = {
379
379
  maxUnusedDays: 30, // Days before unused cache is removed
380
380
  tempThresholdHours: 24, // Hours before temp workspace cache is removed
381
381
  staleProgressHours: 6, // Hours before stuck indexing is considered stale
382
- safetyWindowMinutes: 10, // Minutes of recent activity to never delete
383
- removeDuplicates: true, // Remove duplicate workspace caches
384
- },
385
- watchFiles: DEFAULT_INDEXING_CONFIG.watchFiles,
386
- verbose: DEFAULT_LOGGING_CONFIG.verbose,
387
- memoryLogIntervalMs: DEFAULT_LOGGING_CONFIG.memoryLogIntervalMs,
388
- saveReaderWaitTimeoutMs: DEFAULT_CACHE_CONFIG.saveReaderWaitTimeoutMs,
389
- workerThreads: DEFAULT_WORKER_CONFIG.workerThreads,
390
- workerBatchTimeoutMs: DEFAULT_WORKER_CONFIG.workerBatchTimeoutMs,
391
- workerFailureThreshold: DEFAULT_WORKER_CONFIG.workerFailureThreshold,
392
- workerFailureCooldownMs: DEFAULT_WORKER_CONFIG.workerFailureCooldownMs,
393
- workerMaxChunksPerBatch: DEFAULT_WORKER_CONFIG.workerMaxChunksPerBatch,
394
- allowSingleThreadFallback: DEFAULT_WORKER_CONFIG.allowSingleThreadFallback,
395
- failFastEmbeddingErrors: DEFAULT_WORKER_CONFIG.failFastEmbeddingErrors,
396
- embeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.embeddingProcessPerBatch,
397
- autoEmbeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.autoEmbeddingProcessPerBatch,
398
- embeddingBatchSize: DEFAULT_EMBEDDING_CONFIG.embeddingBatchSize,
399
- embeddingProcessNumThreads: DEFAULT_EMBEDDING_CONFIG.embeddingProcessNumThreads,
400
- embeddingProcessGcRssThresholdMb: DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcRssThresholdMb,
401
- embeddingProcessGcMinIntervalMs: DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcMinIntervalMs,
402
- embeddingProcessGcMaxRequestsWithoutCollection:
403
- DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection,
404
- enableExplicitGc: DEFAULT_MEMORY_CLEANUP_CONFIG.enableExplicitGc,
405
- embeddingModel: DEFAULT_EMBEDDING_CONFIG.embeddingModel,
406
- embeddingDimension: DEFAULT_EMBEDDING_CONFIG.embeddingDimension,
407
- preloadEmbeddingModel: DEFAULT_EMBEDDING_CONFIG.preloadEmbeddingModel,
408
- vectorStoreFormat: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreFormat,
409
- vectorStoreContentMode: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreContentMode,
410
- contentCacheEntries: DEFAULT_VECTOR_STORE_CONFIG.contentCacheEntries,
411
- vectorStoreLoadMode: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreLoadMode,
412
- vectorCacheEntries: DEFAULT_VECTOR_STORE_CONFIG.vectorCacheEntries,
413
- clearCacheAfterIndex: DEFAULT_MEMORY_CLEANUP_CONFIG.clearCacheAfterIndex,
414
- unloadModelAfterIndex: DEFAULT_MEMORY_CLEANUP_CONFIG.unloadModelAfterIndex,
415
- shutdownQueryEmbeddingPoolAfterIndex:
416
- DEFAULT_MEMORY_CLEANUP_CONFIG.shutdownQueryEmbeddingPoolAfterIndex,
417
- unloadModelAfterSearch: DEFAULT_MEMORY_CLEANUP_CONFIG.unloadModelAfterSearch,
418
- embeddingPoolIdleTimeoutMs: DEFAULT_MEMORY_CLEANUP_CONFIG.embeddingPoolIdleTimeoutMs,
419
- incrementalGcThresholdMb: DEFAULT_MEMORY_CLEANUP_CONFIG.incrementalGcThresholdMb,
420
- incrementalMemoryProfile: DEFAULT_MEMORY_CLEANUP_CONFIG.incrementalMemoryProfile,
421
- recycleServerOnHighRssAfterIncremental:
422
- DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssAfterIncremental,
423
- recycleServerOnHighRssThresholdMb:
424
- DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssThresholdMb,
425
- recycleServerOnHighRssCooldownMs:
426
- DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssCooldownMs,
427
- recycleServerOnHighRssDelayMs: DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssDelayMs,
428
- memoryCleanup: { ...DEFAULT_MEMORY_CLEANUP_CONFIG },
429
- semanticWeight: DEFAULT_SEARCH_CONFIG.semanticWeight,
430
- exactMatchBoost: DEFAULT_SEARCH_CONFIG.exactMatchBoost,
431
- recencyBoost: DEFAULT_SEARCH_CONFIG.recencyBoost,
432
- recencyDecayDays: DEFAULT_SEARCH_CONFIG.recencyDecayDays,
433
- textMatchMaxCandidates: DEFAULT_SEARCH_CONFIG.textMatchMaxCandidates,
434
- smartIndexing: DEFAULT_INDEXING_CONFIG.smartIndexing,
435
- callGraphEnabled: DEFAULT_CALL_GRAPH_CONFIG.callGraphEnabled,
436
- callGraphBoost: DEFAULT_CALL_GRAPH_CONFIG.callGraphBoost,
437
- callGraphMaxHops: DEFAULT_CALL_GRAPH_CONFIG.callGraphMaxHops,
438
- annEnabled: DEFAULT_ANN_CONFIG.annEnabled,
439
- annMinChunks: DEFAULT_ANN_CONFIG.annMinChunks,
440
- annMinCandidates: DEFAULT_ANN_CONFIG.annMinCandidates,
441
- annMaxCandidates: DEFAULT_ANN_CONFIG.annMaxCandidates,
442
- annCandidateMultiplier: DEFAULT_ANN_CONFIG.annCandidateMultiplier,
443
- annEfConstruction: DEFAULT_ANN_CONFIG.annEfConstruction,
444
- annEfSearch: DEFAULT_ANN_CONFIG.annEfSearch,
445
- annM: DEFAULT_ANN_CONFIG.annM,
446
- annIndexCache: DEFAULT_ANN_CONFIG.annIndexCache,
447
- annMetric: DEFAULT_ANN_CONFIG.annMetric,
448
- indexing: { ...DEFAULT_INDEXING_CONFIG },
449
- logging: { ...DEFAULT_LOGGING_CONFIG },
450
- cache: { ...DEFAULT_CACHE_CONFIG },
451
- worker: { ...DEFAULT_WORKER_CONFIG },
452
- embedding: { ...DEFAULT_EMBEDDING_CONFIG },
453
- vectorStore: { ...DEFAULT_VECTOR_STORE_CONFIG },
454
- search: { ...DEFAULT_SEARCH_CONFIG },
455
- callGraph: { ...DEFAULT_CALL_GRAPH_CONFIG },
456
- ann: { ...DEFAULT_ANN_CONFIG },
457
- };
458
-
459
- let config = { ...DEFAULT_CONFIG };
460
-
461
- const WORKSPACE_MARKERS = [
382
+ safetyWindowMinutes: 10, // Minutes of recent activity to never delete
383
+ removeDuplicates: true, // Remove duplicate workspace caches
384
+ },
385
+ watchFiles: DEFAULT_INDEXING_CONFIG.watchFiles,
386
+ verbose: DEFAULT_LOGGING_CONFIG.verbose,
387
+ memoryLogIntervalMs: DEFAULT_LOGGING_CONFIG.memoryLogIntervalMs,
388
+ saveReaderWaitTimeoutMs: DEFAULT_CACHE_CONFIG.saveReaderWaitTimeoutMs,
389
+ workerThreads: DEFAULT_WORKER_CONFIG.workerThreads,
390
+ workerBatchTimeoutMs: DEFAULT_WORKER_CONFIG.workerBatchTimeoutMs,
391
+ workerFailureThreshold: DEFAULT_WORKER_CONFIG.workerFailureThreshold,
392
+ workerFailureCooldownMs: DEFAULT_WORKER_CONFIG.workerFailureCooldownMs,
393
+ workerMaxChunksPerBatch: DEFAULT_WORKER_CONFIG.workerMaxChunksPerBatch,
394
+ allowSingleThreadFallback: DEFAULT_WORKER_CONFIG.allowSingleThreadFallback,
395
+ failFastEmbeddingErrors: DEFAULT_WORKER_CONFIG.failFastEmbeddingErrors,
396
+ embeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.embeddingProcessPerBatch,
397
+ autoEmbeddingProcessPerBatch: DEFAULT_EMBEDDING_CONFIG.autoEmbeddingProcessPerBatch,
398
+ embeddingBatchSize: DEFAULT_EMBEDDING_CONFIG.embeddingBatchSize,
399
+ embeddingProcessNumThreads: DEFAULT_EMBEDDING_CONFIG.embeddingProcessNumThreads,
400
+ embeddingProcessGcRssThresholdMb: DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcRssThresholdMb,
401
+ embeddingProcessGcMinIntervalMs: DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcMinIntervalMs,
402
+ embeddingProcessGcMaxRequestsWithoutCollection:
403
+ DEFAULT_EMBEDDING_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection,
404
+ enableExplicitGc: DEFAULT_MEMORY_CLEANUP_CONFIG.enableExplicitGc,
405
+ embeddingModel: DEFAULT_EMBEDDING_CONFIG.embeddingModel,
406
+ embeddingDimension: DEFAULT_EMBEDDING_CONFIG.embeddingDimension,
407
+ preloadEmbeddingModel: DEFAULT_EMBEDDING_CONFIG.preloadEmbeddingModel,
408
+ vectorStoreFormat: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreFormat,
409
+ vectorStoreContentMode: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreContentMode,
410
+ contentCacheEntries: DEFAULT_VECTOR_STORE_CONFIG.contentCacheEntries,
411
+ vectorStoreLoadMode: DEFAULT_VECTOR_STORE_CONFIG.vectorStoreLoadMode,
412
+ vectorCacheEntries: DEFAULT_VECTOR_STORE_CONFIG.vectorCacheEntries,
413
+ clearCacheAfterIndex: DEFAULT_MEMORY_CLEANUP_CONFIG.clearCacheAfterIndex,
414
+ unloadModelAfterIndex: DEFAULT_MEMORY_CLEANUP_CONFIG.unloadModelAfterIndex,
415
+ shutdownQueryEmbeddingPoolAfterIndex:
416
+ DEFAULT_MEMORY_CLEANUP_CONFIG.shutdownQueryEmbeddingPoolAfterIndex,
417
+ unloadModelAfterSearch: DEFAULT_MEMORY_CLEANUP_CONFIG.unloadModelAfterSearch,
418
+ embeddingPoolIdleTimeoutMs: DEFAULT_MEMORY_CLEANUP_CONFIG.embeddingPoolIdleTimeoutMs,
419
+ incrementalGcThresholdMb: DEFAULT_MEMORY_CLEANUP_CONFIG.incrementalGcThresholdMb,
420
+ incrementalMemoryProfile: DEFAULT_MEMORY_CLEANUP_CONFIG.incrementalMemoryProfile,
421
+ recycleServerOnHighRssAfterIncremental:
422
+ DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssAfterIncremental,
423
+ recycleServerOnHighRssThresholdMb:
424
+ DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssThresholdMb,
425
+ recycleServerOnHighRssCooldownMs:
426
+ DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssCooldownMs,
427
+ recycleServerOnHighRssDelayMs: DEFAULT_MEMORY_CLEANUP_CONFIG.recycleServerOnHighRssDelayMs,
428
+ memoryCleanup: { ...DEFAULT_MEMORY_CLEANUP_CONFIG },
429
+ semanticWeight: DEFAULT_SEARCH_CONFIG.semanticWeight,
430
+ exactMatchBoost: DEFAULT_SEARCH_CONFIG.exactMatchBoost,
431
+ recencyBoost: DEFAULT_SEARCH_CONFIG.recencyBoost,
432
+ recencyDecayDays: DEFAULT_SEARCH_CONFIG.recencyDecayDays,
433
+ textMatchMaxCandidates: DEFAULT_SEARCH_CONFIG.textMatchMaxCandidates,
434
+ smartIndexing: DEFAULT_INDEXING_CONFIG.smartIndexing,
435
+ callGraphEnabled: DEFAULT_CALL_GRAPH_CONFIG.callGraphEnabled,
436
+ callGraphBoost: DEFAULT_CALL_GRAPH_CONFIG.callGraphBoost,
437
+ callGraphMaxHops: DEFAULT_CALL_GRAPH_CONFIG.callGraphMaxHops,
438
+ annEnabled: DEFAULT_ANN_CONFIG.annEnabled,
439
+ annMinChunks: DEFAULT_ANN_CONFIG.annMinChunks,
440
+ annMinCandidates: DEFAULT_ANN_CONFIG.annMinCandidates,
441
+ annMaxCandidates: DEFAULT_ANN_CONFIG.annMaxCandidates,
442
+ annCandidateMultiplier: DEFAULT_ANN_CONFIG.annCandidateMultiplier,
443
+ annEfConstruction: DEFAULT_ANN_CONFIG.annEfConstruction,
444
+ annEfSearch: DEFAULT_ANN_CONFIG.annEfSearch,
445
+ annM: DEFAULT_ANN_CONFIG.annM,
446
+ annIndexCache: DEFAULT_ANN_CONFIG.annIndexCache,
447
+ annMetric: DEFAULT_ANN_CONFIG.annMetric,
448
+ indexing: { ...DEFAULT_INDEXING_CONFIG },
449
+ logging: { ...DEFAULT_LOGGING_CONFIG },
450
+ cache: { ...DEFAULT_CACHE_CONFIG },
451
+ worker: { ...DEFAULT_WORKER_CONFIG },
452
+ embedding: { ...DEFAULT_EMBEDDING_CONFIG },
453
+ vectorStore: { ...DEFAULT_VECTOR_STORE_CONFIG },
454
+ search: { ...DEFAULT_SEARCH_CONFIG },
455
+ callGraph: { ...DEFAULT_CALL_GRAPH_CONFIG },
456
+ ann: { ...DEFAULT_ANN_CONFIG },
457
+ };
458
+
459
+ let config = { ...DEFAULT_CONFIG };
460
+
461
+ const WORKSPACE_MARKERS = [
462
462
  '.git',
463
463
  'package.json',
464
464
  'pyproject.toml',
@@ -470,118 +470,118 @@ const WORKSPACE_MARKERS = [
470
470
  'requirements.txt',
471
471
  'Gemfile',
472
472
  'Makefile',
473
- 'CMakeLists.txt',
474
- ];
475
-
476
- function hasOwn(obj, key) {
477
- return Object.prototype.hasOwnProperty.call(obj, key);
478
- }
479
-
480
- const CONFIG_NAMESPACES = Object.freeze([
481
- {
482
- name: 'memoryCleanup',
483
- keys: MEMORY_CLEANUP_KEYS,
484
- defaults: DEFAULT_MEMORY_CLEANUP_CONFIG,
485
- },
486
- {
487
- name: 'indexing',
488
- keys: INDEXING_KEYS,
489
- defaults: DEFAULT_INDEXING_CONFIG,
490
- },
491
- {
492
- name: 'logging',
493
- keys: LOGGING_KEYS,
494
- defaults: DEFAULT_LOGGING_CONFIG,
495
- },
496
- {
497
- name: 'cache',
498
- keys: CACHE_KEYS,
499
- defaults: DEFAULT_CACHE_CONFIG,
500
- },
501
- {
502
- name: 'worker',
503
- keys: WORKER_KEYS,
504
- defaults: DEFAULT_WORKER_CONFIG,
505
- },
506
- {
507
- name: 'embedding',
508
- keys: EMBEDDING_KEYS,
509
- defaults: DEFAULT_EMBEDDING_CONFIG,
510
- },
511
- {
512
- name: 'vectorStore',
513
- keys: VECTOR_STORE_KEYS,
514
- defaults: DEFAULT_VECTOR_STORE_CONFIG,
515
- },
516
- {
517
- name: 'search',
518
- keys: SEARCH_KEYS,
519
- defaults: DEFAULT_SEARCH_CONFIG,
520
- },
521
- {
522
- name: 'callGraph',
523
- keys: CALL_GRAPH_KEYS,
524
- defaults: DEFAULT_CALL_GRAPH_CONFIG,
525
- },
526
- {
527
- name: 'ann',
528
- keys: ANN_KEYS,
529
- defaults: DEFAULT_ANN_CONFIG,
530
- },
531
- ]);
532
-
533
- function applyNamespace(targetConfig, sourceConfig, namespaceName, keys, defaults) {
534
- const sourceNamespace =
535
- sourceConfig && typeof sourceConfig[namespaceName] === 'object'
536
- ? sourceConfig[namespaceName]
537
- : {};
538
- const mergedNamespace = {
539
- ...defaults,
540
- ...(targetConfig[namespaceName] && typeof targetConfig[namespaceName] === 'object'
541
- ? targetConfig[namespaceName]
542
- : {}),
543
- };
544
-
545
- for (const key of keys) {
546
- if (hasOwn(sourceNamespace, key)) {
547
- targetConfig[key] = mergedNamespace[key];
548
- } else {
549
- mergedNamespace[key] = targetConfig[key];
550
- }
551
- }
552
-
553
- targetConfig[namespaceName] = mergedNamespace;
554
- }
555
-
556
- function syncNamespace(targetConfig, namespaceName, keys, defaults) {
557
- const currentNamespace =
558
- targetConfig[namespaceName] && typeof targetConfig[namespaceName] === 'object'
559
- ? targetConfig[namespaceName]
560
- : {};
561
- const mergedNamespace = { ...defaults, ...currentNamespace };
562
- for (const key of keys) {
563
- mergedNamespace[key] = targetConfig[key];
564
- }
565
- targetConfig[namespaceName] = mergedNamespace;
566
- }
567
-
568
- function applyAllNamespaces(targetConfig, sourceConfig) {
569
- for (const namespace of CONFIG_NAMESPACES) {
570
- applyNamespace(
571
- targetConfig,
572
- sourceConfig,
573
- namespace.name,
574
- namespace.keys,
575
- namespace.defaults
576
- );
577
- }
578
- }
579
-
580
- function syncAllNamespaces(targetConfig) {
581
- for (const namespace of CONFIG_NAMESPACES) {
582
- syncNamespace(targetConfig, namespace.name, namespace.keys, namespace.defaults);
583
- }
584
- }
473
+ 'CMakeLists.txt',
474
+ ];
475
+
476
+ function hasOwn(obj, key) {
477
+ return Object.prototype.hasOwnProperty.call(obj, key);
478
+ }
479
+
480
+ const CONFIG_NAMESPACES = Object.freeze([
481
+ {
482
+ name: 'memoryCleanup',
483
+ keys: MEMORY_CLEANUP_KEYS,
484
+ defaults: DEFAULT_MEMORY_CLEANUP_CONFIG,
485
+ },
486
+ {
487
+ name: 'indexing',
488
+ keys: INDEXING_KEYS,
489
+ defaults: DEFAULT_INDEXING_CONFIG,
490
+ },
491
+ {
492
+ name: 'logging',
493
+ keys: LOGGING_KEYS,
494
+ defaults: DEFAULT_LOGGING_CONFIG,
495
+ },
496
+ {
497
+ name: 'cache',
498
+ keys: CACHE_KEYS,
499
+ defaults: DEFAULT_CACHE_CONFIG,
500
+ },
501
+ {
502
+ name: 'worker',
503
+ keys: WORKER_KEYS,
504
+ defaults: DEFAULT_WORKER_CONFIG,
505
+ },
506
+ {
507
+ name: 'embedding',
508
+ keys: EMBEDDING_KEYS,
509
+ defaults: DEFAULT_EMBEDDING_CONFIG,
510
+ },
511
+ {
512
+ name: 'vectorStore',
513
+ keys: VECTOR_STORE_KEYS,
514
+ defaults: DEFAULT_VECTOR_STORE_CONFIG,
515
+ },
516
+ {
517
+ name: 'search',
518
+ keys: SEARCH_KEYS,
519
+ defaults: DEFAULT_SEARCH_CONFIG,
520
+ },
521
+ {
522
+ name: 'callGraph',
523
+ keys: CALL_GRAPH_KEYS,
524
+ defaults: DEFAULT_CALL_GRAPH_CONFIG,
525
+ },
526
+ {
527
+ name: 'ann',
528
+ keys: ANN_KEYS,
529
+ defaults: DEFAULT_ANN_CONFIG,
530
+ },
531
+ ]);
532
+
533
+ function applyNamespace(targetConfig, sourceConfig, namespaceName, keys, defaults) {
534
+ const sourceNamespace =
535
+ sourceConfig && typeof sourceConfig[namespaceName] === 'object'
536
+ ? sourceConfig[namespaceName]
537
+ : {};
538
+ const mergedNamespace = {
539
+ ...defaults,
540
+ ...(targetConfig[namespaceName] && typeof targetConfig[namespaceName] === 'object'
541
+ ? targetConfig[namespaceName]
542
+ : {}),
543
+ };
544
+
545
+ for (const key of keys) {
546
+ if (hasOwn(sourceNamespace, key)) {
547
+ targetConfig[key] = mergedNamespace[key];
548
+ } else {
549
+ mergedNamespace[key] = targetConfig[key];
550
+ }
551
+ }
552
+
553
+ targetConfig[namespaceName] = mergedNamespace;
554
+ }
555
+
556
+ function syncNamespace(targetConfig, namespaceName, keys, defaults) {
557
+ const currentNamespace =
558
+ targetConfig[namespaceName] && typeof targetConfig[namespaceName] === 'object'
559
+ ? targetConfig[namespaceName]
560
+ : {};
561
+ const mergedNamespace = { ...defaults, ...currentNamespace };
562
+ for (const key of keys) {
563
+ mergedNamespace[key] = targetConfig[key];
564
+ }
565
+ targetConfig[namespaceName] = mergedNamespace;
566
+ }
567
+
568
+ function applyAllNamespaces(targetConfig, sourceConfig) {
569
+ for (const namespace of CONFIG_NAMESPACES) {
570
+ applyNamespace(
571
+ targetConfig,
572
+ sourceConfig,
573
+ namespace.name,
574
+ namespace.keys,
575
+ namespace.defaults
576
+ );
577
+ }
578
+ }
579
+
580
+ function syncAllNamespaces(targetConfig) {
581
+ for (const namespace of CONFIG_NAMESPACES) {
582
+ syncNamespace(targetConfig, namespace.name, namespace.keys, namespace.defaults);
583
+ }
584
+ }
585
585
 
586
586
  async function pathExists(filePath) {
587
587
  try {
@@ -602,7 +602,7 @@ async function readConfigFile(filePath) {
602
602
  }
603
603
  }
604
604
 
605
- async function findWorkspaceRoot(startDir) {
605
+ async function findWorkspaceRoot(startDir) {
606
606
  let current = path.resolve(startDir);
607
607
  while (true) {
608
608
  for (const marker of WORKSPACE_MARKERS) {
@@ -614,113 +614,113 @@ async function findWorkspaceRoot(startDir) {
614
614
  if (parent === current) break;
615
615
  current = parent;
616
616
  }
617
- return path.resolve(startDir);
618
- }
619
-
620
- async function resolveWorkspaceCandidate(rawValue) {
621
- if (!rawValue || rawValue.includes('${')) return null;
622
- const candidate = path.resolve(rawValue);
623
- if (!(await pathExists(candidate))) return null;
624
- try {
625
- const stats = await fs.stat(candidate);
626
- if (!stats.isDirectory()) return null;
627
- } catch {
628
- return null;
629
- }
630
- return candidate;
631
- }
632
-
633
- function logWorkspaceResolution(resolution) {
634
- if (!resolution || !resolution.path) return;
635
-
636
- if (resolution.source === 'workspace-arg') {
637
- console.info(`[Config] Workspace resolution: --workspace -> ${resolution.path}`);
638
- return;
639
- }
640
-
641
- if (resolution.source === 'env' && resolution.envKey) {
642
- console.info(`[Config] Workspace resolution: env ${resolution.envKey} -> ${resolution.path}`);
643
- return;
644
- }
645
-
646
- if (resolution.source === 'test-cwd') {
647
- console.info(`[Config] Workspace resolution: process.cwd() (test mode) -> ${resolution.path}`);
648
- return;
649
- }
650
-
651
- if (resolution.source === 'cwd-root-search') {
652
- const from = resolution.fromPath || process.cwd();
653
- console.info(
654
- `[Config] Workspace resolution: workspace root from cwd (${from}) -> ${resolution.path}`
655
- );
656
- return;
657
- }
658
-
659
- console.info(`[Config] Workspace resolution: process.cwd() -> ${resolution.path}`);
660
- }
661
-
662
- async function resolveWorkspaceDir(workspaceDir) {
663
- if (workspaceDir) {
664
- return {
665
- path: path.resolve(workspaceDir),
666
- source: 'workspace-arg',
667
- };
668
- }
669
- if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
670
- return {
671
- path: path.resolve(process.cwd()),
672
- source: 'test-cwd',
673
- };
674
- }
675
-
676
- for (const key of getWorkspaceEnvKeys()) {
677
- const candidate = await resolveWorkspaceCandidate(process.env[key]);
678
- if (candidate) {
679
- return {
680
- path: candidate,
681
- source: 'env',
682
- envKey: key,
683
- };
684
- }
685
- }
686
-
687
- const cwd = path.resolve(process.cwd());
688
- const root = await findWorkspaceRoot(cwd);
689
- if (root !== cwd) {
690
- return {
691
- path: root,
692
- source: 'cwd-root-search',
693
- fromPath: cwd,
694
- };
695
- }
696
- return {
697
- path: cwd,
698
- source: 'cwd',
699
- };
700
- }
701
-
702
- export async function loadConfig(workspaceDir = null) {
703
- try {
617
+ return path.resolve(startDir);
618
+ }
619
+
620
+ async function resolveWorkspaceCandidate(rawValue) {
621
+ if (!rawValue || rawValue.includes('${')) return null;
622
+ const candidate = path.resolve(rawValue);
623
+ if (!(await pathExists(candidate))) return null;
624
+ try {
625
+ const stats = await fs.stat(candidate);
626
+ if (!stats.isDirectory()) return null;
627
+ } catch {
628
+ return null;
629
+ }
630
+ return candidate;
631
+ }
632
+
633
+ function logWorkspaceResolution(resolution) {
634
+ if (!resolution || !resolution.path) return;
635
+
636
+ if (resolution.source === 'workspace-arg') {
637
+ console.info(`[Config] Workspace resolution: --workspace -> ${resolution.path}`);
638
+ return;
639
+ }
640
+
641
+ if (resolution.source === 'env' && resolution.envKey) {
642
+ console.info(`[Config] Workspace resolution: env ${resolution.envKey} -> ${resolution.path}`);
643
+ return;
644
+ }
645
+
646
+ if (resolution.source === 'test-cwd') {
647
+ console.info(`[Config] Workspace resolution: process.cwd() (test mode) -> ${resolution.path}`);
648
+ return;
649
+ }
650
+
651
+ if (resolution.source === 'cwd-root-search') {
652
+ const from = resolution.fromPath || process.cwd();
653
+ console.info(
654
+ `[Config] Workspace resolution: workspace root from cwd (${from}) -> ${resolution.path}`
655
+ );
656
+ return;
657
+ }
658
+
659
+ console.info(`[Config] Workspace resolution: process.cwd() -> ${resolution.path}`);
660
+ }
661
+
662
+ async function resolveWorkspaceDir(workspaceDir) {
663
+ if (workspaceDir) {
664
+ return {
665
+ path: path.resolve(workspaceDir),
666
+ source: 'workspace-arg',
667
+ };
668
+ }
669
+ if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
670
+ return {
671
+ path: path.resolve(process.cwd()),
672
+ source: 'test-cwd',
673
+ };
674
+ }
675
+
676
+ for (const key of getWorkspaceEnvKeys()) {
677
+ const candidate = await resolveWorkspaceCandidate(process.env[key]);
678
+ if (candidate) {
679
+ return {
680
+ path: candidate,
681
+ source: 'env',
682
+ envKey: key,
683
+ };
684
+ }
685
+ }
686
+
687
+ const cwd = path.resolve(process.cwd());
688
+ const root = await findWorkspaceRoot(cwd);
689
+ if (root !== cwd) {
690
+ return {
691
+ path: root,
692
+ source: 'cwd-root-search',
693
+ fromPath: cwd,
694
+ };
695
+ }
696
+ return {
697
+ path: cwd,
698
+ source: 'cwd',
699
+ };
700
+ }
701
+
702
+ export async function loadConfig(workspaceDir = null) {
703
+ try {
704
704
  // Determine the base directory for configuration
705
705
  let baseDir;
706
706
  let configPath;
707
-
708
- let serverDir = null;
709
- if (workspaceDir) {
710
- // Workspace mode: load config from workspace root
711
- const workspaceResolution = await resolveWorkspaceDir(workspaceDir);
712
- baseDir = workspaceResolution.path;
713
- console.info(`[Config] Workspace mode: ${baseDir}`);
714
- logWorkspaceResolution(workspaceResolution);
715
- } else {
716
- // Server mode: load config from server directory for global settings,
717
- // but use process.cwd() as base for searching if not specified otherwise
718
- const scriptDir = path.dirname(fileURLToPath(import.meta.url));
719
- serverDir = path.resolve(scriptDir, '..');
720
- const workspaceResolution = await resolveWorkspaceDir(null);
721
- baseDir = workspaceResolution.path;
722
- logWorkspaceResolution(workspaceResolution);
723
- }
707
+
708
+ let serverDir = null;
709
+ if (workspaceDir) {
710
+ // Workspace mode: load config from workspace root
711
+ const workspaceResolution = await resolveWorkspaceDir(workspaceDir);
712
+ baseDir = workspaceResolution.path;
713
+ console.info(`[Config] Workspace mode: ${baseDir}`);
714
+ logWorkspaceResolution(workspaceResolution);
715
+ } else {
716
+ // Server mode: load config from server directory for global settings,
717
+ // but use process.cwd() as base for searching if not specified otherwise
718
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
719
+ serverDir = path.resolve(scriptDir, '..');
720
+ const workspaceResolution = await resolveWorkspaceDir(null);
721
+ baseDir = workspaceResolution.path;
722
+ logWorkspaceResolution(workspaceResolution);
723
+ }
724
724
 
725
725
  let userConfig = {};
726
726
  const configNames = ['config.jsonc', 'config.json'];
@@ -759,16 +759,16 @@ export async function loadConfig(workspaceDir = null) {
759
759
  }
760
760
  }
761
761
 
762
- config = { ...DEFAULT_CONFIG, ...userConfig };
763
- applyAllNamespaces(config, userConfig);
764
-
765
- // Backward compatibility for legacy top-level cache cleanup toggle.
766
- if (
767
- hasOwn(userConfig, 'autoCleanStaleCaches') &&
768
- !(userConfig.cacheCleanup && hasOwn(userConfig.cacheCleanup, 'autoCleanup'))
769
- ) {
770
- config.cacheCleanup.autoCleanup = Boolean(userConfig.autoCleanStaleCaches);
771
- }
762
+ config = { ...DEFAULT_CONFIG, ...userConfig };
763
+ applyAllNamespaces(config, userConfig);
764
+
765
+ // Backward compatibility for legacy top-level cache cleanup toggle.
766
+ if (
767
+ hasOwn(userConfig, 'autoCleanStaleCaches') &&
768
+ !(userConfig.cacheCleanup && hasOwn(userConfig.cacheCleanup, 'autoCleanup'))
769
+ ) {
770
+ config.cacheCleanup.autoCleanup = Boolean(userConfig.autoCleanStaleCaches);
771
+ }
772
772
 
773
773
  // Set search directory (respect user override when provided)
774
774
  if (userConfig.searchDirectory) {
@@ -1050,74 +1050,74 @@ export async function loadConfig(workspaceDir = null) {
1050
1050
  }
1051
1051
  }
1052
1052
 
1053
- if (process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB !== undefined) {
1054
- const value = parseInt(process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB, 10);
1055
- if (!isNaN(value) && value >= 0) {
1056
- config.incrementalGcThresholdMb = value;
1053
+ if (process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB !== undefined) {
1054
+ const value = parseInt(process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB, 10);
1055
+ if (!isNaN(value) && value >= 0) {
1056
+ config.incrementalGcThresholdMb = value;
1057
1057
  } else {
1058
1058
  console.warn(
1059
1059
  `[Config] Invalid SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB: ${process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB}, using default`
1060
- );
1061
- }
1062
- }
1063
-
1064
- if (process.env.SMART_CODING_INCREMENTAL_MEMORY_PROFILE !== undefined) {
1065
- const value = process.env.SMART_CODING_INCREMENTAL_MEMORY_PROFILE;
1066
- if (value === 'true' || value === 'false') {
1067
- config.incrementalMemoryProfile = value === 'true';
1068
- } else {
1069
- console.warn(
1070
- `[Config] Invalid SMART_CODING_INCREMENTAL_MEMORY_PROFILE: ${value}, using default`
1071
- );
1072
- }
1073
- }
1074
-
1075
- if (process.env.SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL !== undefined) {
1076
- const value = process.env.SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL;
1077
- if (value === 'true' || value === 'false') {
1078
- config.recycleServerOnHighRssAfterIncremental = value === 'true';
1079
- } else {
1080
- console.warn(
1081
- `[Config] Invalid SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL: ${value}, using default`
1082
- );
1083
- }
1084
- }
1085
-
1086
- if (process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB !== undefined) {
1087
- const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB, 10);
1088
- if (!isNaN(value) && value > 0) {
1089
- config.recycleServerOnHighRssThresholdMb = value;
1090
- } else {
1091
- console.warn(
1092
- `[Config] Invalid SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB: ${process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB}, using default`
1093
- );
1094
- }
1095
- }
1096
-
1097
- if (process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS !== undefined) {
1098
- const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS, 10);
1099
- if (!isNaN(value) && value >= 0) {
1100
- config.recycleServerOnHighRssCooldownMs = value;
1101
- } else {
1102
- console.warn(
1103
- `[Config] Invalid SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS: ${process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS}, using default`
1104
- );
1105
- }
1106
- }
1107
-
1108
- if (process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS !== undefined) {
1109
- const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS, 10);
1110
- if (!isNaN(value) && value >= 0) {
1111
- config.recycleServerOnHighRssDelayMs = value;
1112
- } else {
1113
- console.warn(
1114
- `[Config] Invalid SMART_CODING_RECYCLE_SERVER_DELAY_MS: ${process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS}, using default`
1115
- );
1116
- }
1117
- }
1118
-
1119
- if (process.env.SMART_CODING_CONTENT_CACHE_ENTRIES !== undefined) {
1120
- const value = parseInt(process.env.SMART_CODING_CONTENT_CACHE_ENTRIES, 10);
1060
+ );
1061
+ }
1062
+ }
1063
+
1064
+ if (process.env.SMART_CODING_INCREMENTAL_MEMORY_PROFILE !== undefined) {
1065
+ const value = process.env.SMART_CODING_INCREMENTAL_MEMORY_PROFILE;
1066
+ if (value === 'true' || value === 'false') {
1067
+ config.incrementalMemoryProfile = value === 'true';
1068
+ } else {
1069
+ console.warn(
1070
+ `[Config] Invalid SMART_CODING_INCREMENTAL_MEMORY_PROFILE: ${value}, using default`
1071
+ );
1072
+ }
1073
+ }
1074
+
1075
+ if (process.env.SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL !== undefined) {
1076
+ const value = process.env.SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL;
1077
+ if (value === 'true' || value === 'false') {
1078
+ config.recycleServerOnHighRssAfterIncremental = value === 'true';
1079
+ } else {
1080
+ console.warn(
1081
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_ON_HIGH_RSS_AFTER_INCREMENTAL: ${value}, using default`
1082
+ );
1083
+ }
1084
+ }
1085
+
1086
+ if (process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB !== undefined) {
1087
+ const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB, 10);
1088
+ if (!isNaN(value) && value > 0) {
1089
+ config.recycleServerOnHighRssThresholdMb = value;
1090
+ } else {
1091
+ console.warn(
1092
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB: ${process.env.SMART_CODING_RECYCLE_SERVER_RSS_THRESHOLD_MB}, using default`
1093
+ );
1094
+ }
1095
+ }
1096
+
1097
+ if (process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS !== undefined) {
1098
+ const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS, 10);
1099
+ if (!isNaN(value) && value >= 0) {
1100
+ config.recycleServerOnHighRssCooldownMs = value;
1101
+ } else {
1102
+ console.warn(
1103
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS: ${process.env.SMART_CODING_RECYCLE_SERVER_COOLDOWN_MS}, using default`
1104
+ );
1105
+ }
1106
+ }
1107
+
1108
+ if (process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS !== undefined) {
1109
+ const value = parseInt(process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS, 10);
1110
+ if (!isNaN(value) && value >= 0) {
1111
+ config.recycleServerOnHighRssDelayMs = value;
1112
+ } else {
1113
+ console.warn(
1114
+ `[Config] Invalid SMART_CODING_RECYCLE_SERVER_DELAY_MS: ${process.env.SMART_CODING_RECYCLE_SERVER_DELAY_MS}, using default`
1115
+ );
1116
+ }
1117
+ }
1118
+
1119
+ if (process.env.SMART_CODING_CONTENT_CACHE_ENTRIES !== undefined) {
1120
+ const value = parseInt(process.env.SMART_CODING_CONTENT_CACHE_ENTRIES, 10);
1121
1121
  if (!isNaN(value) && value >= 0 && value <= 10000) {
1122
1122
  config.contentCacheEntries = value;
1123
1123
  } else {
@@ -1414,9 +1414,9 @@ export async function loadConfig(workspaceDir = null) {
1414
1414
  }
1415
1415
  }
1416
1416
 
1417
- if (
1418
- config.embeddingProcessGcMaxRequestsWithoutCollection !== null &&
1419
- config.embeddingProcessGcMaxRequestsWithoutCollection !== undefined
1417
+ if (
1418
+ config.embeddingProcessGcMaxRequestsWithoutCollection !== null &&
1419
+ config.embeddingProcessGcMaxRequestsWithoutCollection !== undefined
1420
1420
  ) {
1421
1421
  const value = parseInt(config.embeddingProcessGcMaxRequestsWithoutCollection, 10);
1422
1422
  if (!isNaN(value) && value > 0) {
@@ -1425,14 +1425,14 @@ export async function loadConfig(workspaceDir = null) {
1425
1425
  console.warn(
1426
1426
  `[Config] Invalid embeddingProcessGcMaxRequestsWithoutCollection: ${config.embeddingProcessGcMaxRequestsWithoutCollection}, using default`
1427
1427
  );
1428
- config.embeddingProcessGcMaxRequestsWithoutCollection =
1429
- DEFAULT_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection;
1430
- }
1431
- }
1432
-
1433
- syncAllNamespaces(config);
1434
- return config;
1435
- }
1428
+ config.embeddingProcessGcMaxRequestsWithoutCollection =
1429
+ DEFAULT_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection;
1430
+ }
1431
+ }
1432
+
1433
+ syncAllNamespaces(config);
1434
+ return config;
1435
+ }
1436
1436
 
1437
1437
  /**
1438
1438
  * Get platform-specific global cache directory