@softerist/heuristic-mcp 3.0.14 → 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 +90 -82
  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 +136 -69
  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 +487 -427
  20. package/lib/constants.js +31 -0
  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 -638
  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 -0
  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
@@ -5,127 +5,128 @@ 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
- 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 = {
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 = {
129
130
  searchDirectory: '.',
130
131
  fileExtensions: [
131
132
  // JavaScript/TypeScript
@@ -361,16 +362,16 @@ const DEFAULT_CONFIG = {
361
362
  '**/scripts/**',
362
363
  '**/tools/**',
363
364
  ],
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: {
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: {
374
375
  autoCleanup: true, // Automatically remove stale caches on startup
375
376
  staleNoMetaHours: 6, // Hours before incomplete cache (no meta.json) is considered stale
376
377
  emptyThresholdHours: 24, // Hours before empty cache (0 files/chunks) is removed
@@ -378,97 +379,86 @@ const DEFAULT_CONFIG = {
378
379
  maxUnusedDays: 30, // Days before unused cache is removed
379
380
  tempThresholdHours: 24, // Hours before temp workspace cache is removed
380
381
  staleProgressHours: 6, // Hours before stuck indexing is considered stale
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
- };
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
+ };
457
458
 
458
459
  let config = { ...DEFAULT_CONFIG };
459
460
 
460
- const WORKSPACE_ENV_VARS = [
461
- 'HEURISTIC_MCP_WORKSPACE',
462
- 'MCP_WORKSPACE',
463
- 'WORKSPACE_FOLDER',
464
- 'WORKSPACE_ROOT',
465
- 'CURSOR_WORKSPACE',
466
- 'CLAUDE_WORKSPACE',
467
- 'ANTIGRAVITY_WORKSPACE',
468
- 'INIT_CWD',
469
- ];
470
-
471
- const WORKSPACE_MARKERS = [
461
+ const WORKSPACE_MARKERS = [
472
462
  '.git',
473
463
  'package.json',
474
464
  'pyproject.toml',
@@ -480,118 +470,118 @@ const WORKSPACE_MARKERS = [
480
470
  'requirements.txt',
481
471
  'Gemfile',
482
472
  'Makefile',
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
- }
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
+ }
595
585
 
596
586
  async function pathExists(filePath) {
597
587
  try {
@@ -627,20 +617,86 @@ async function findWorkspaceRoot(startDir) {
627
617
  return path.resolve(startDir);
628
618
  }
629
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
+
630
662
  async function resolveWorkspaceDir(workspaceDir) {
631
- if (workspaceDir) return path.resolve(workspaceDir);
663
+ if (workspaceDir) {
664
+ return {
665
+ path: path.resolve(workspaceDir),
666
+ source: 'workspace-arg',
667
+ };
668
+ }
632
669
  if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
633
- return path.resolve(process.cwd());
670
+ return {
671
+ path: path.resolve(process.cwd()),
672
+ source: 'test-cwd',
673
+ };
634
674
  }
635
675
 
636
- for (const key of WORKSPACE_ENV_VARS) {
637
- const value = process.env[key];
638
- if (!value || value.includes('${')) continue;
639
- const candidate = path.resolve(value);
640
- if (await pathExists(candidate)) return candidate;
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
+ }
641
685
  }
642
686
 
643
- return await findWorkspaceRoot(process.cwd());
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
+ };
644
700
  }
645
701
 
646
702
  export async function loadConfig(workspaceDir = null) {
@@ -652,14 +708,18 @@ export async function loadConfig(workspaceDir = null) {
652
708
  let serverDir = null;
653
709
  if (workspaceDir) {
654
710
  // Workspace mode: load config from workspace root
655
- baseDir = path.resolve(workspaceDir);
711
+ const workspaceResolution = await resolveWorkspaceDir(workspaceDir);
712
+ baseDir = workspaceResolution.path;
656
713
  console.info(`[Config] Workspace mode: ${baseDir}`);
714
+ logWorkspaceResolution(workspaceResolution);
657
715
  } else {
658
716
  // Server mode: load config from server directory for global settings,
659
717
  // but use process.cwd() as base for searching if not specified otherwise
660
718
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
661
719
  serverDir = path.resolve(scriptDir, '..');
662
- baseDir = await resolveWorkspaceDir(null);
720
+ const workspaceResolution = await resolveWorkspaceDir(null);
721
+ baseDir = workspaceResolution.path;
722
+ logWorkspaceResolution(workspaceResolution);
663
723
  }
664
724
 
665
725
  let userConfig = {};
@@ -699,16 +759,16 @@ export async function loadConfig(workspaceDir = null) {
699
759
  }
700
760
  }
701
761
 
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
- }
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
+ }
712
772
 
713
773
  // Set search directory (respect user override when provided)
714
774
  if (userConfig.searchDirectory) {
@@ -990,74 +1050,74 @@ export async function loadConfig(workspaceDir = null) {
990
1050
  }
991
1051
  }
992
1052
 
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;
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;
997
1057
  } else {
998
1058
  console.warn(
999
1059
  `[Config] Invalid SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB: ${process.env.SMART_CODING_INCREMENTAL_GC_THRESHOLD_MB}, using default`
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);
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);
1061
1121
  if (!isNaN(value) && value >= 0 && value <= 10000) {
1062
1122
  config.contentCacheEntries = value;
1063
1123
  } else {
@@ -1354,9 +1414,9 @@ export async function loadConfig(workspaceDir = null) {
1354
1414
  }
1355
1415
  }
1356
1416
 
1357
- if (
1358
- config.embeddingProcessGcMaxRequestsWithoutCollection !== null &&
1359
- config.embeddingProcessGcMaxRequestsWithoutCollection !== undefined
1417
+ if (
1418
+ config.embeddingProcessGcMaxRequestsWithoutCollection !== null &&
1419
+ config.embeddingProcessGcMaxRequestsWithoutCollection !== undefined
1360
1420
  ) {
1361
1421
  const value = parseInt(config.embeddingProcessGcMaxRequestsWithoutCollection, 10);
1362
1422
  if (!isNaN(value) && value > 0) {
@@ -1365,14 +1425,14 @@ export async function loadConfig(workspaceDir = null) {
1365
1425
  console.warn(
1366
1426
  `[Config] Invalid embeddingProcessGcMaxRequestsWithoutCollection: ${config.embeddingProcessGcMaxRequestsWithoutCollection}, using default`
1367
1427
  );
1368
- config.embeddingProcessGcMaxRequestsWithoutCollection =
1369
- DEFAULT_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection;
1370
- }
1371
- }
1372
-
1373
- syncAllNamespaces(config);
1374
- return config;
1375
- }
1428
+ config.embeddingProcessGcMaxRequestsWithoutCollection =
1429
+ DEFAULT_CONFIG.embeddingProcessGcMaxRequestsWithoutCollection;
1430
+ }
1431
+ }
1432
+
1433
+ syncAllNamespaces(config);
1434
+ return config;
1435
+ }
1376
1436
 
1377
1437
  /**
1378
1438
  * Get platform-specific global cache directory