@softerist/heuristic-mcp 3.2.8 → 3.2.10

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.
@@ -139,6 +139,9 @@ export class CodebaseIndexer {
139
139
  this._lastExplicitGcAt = 0;
140
140
  this._lastHighRssRecycleAt = 0;
141
141
  this._pendingHighRssRecycleTimer = null;
142
+ this._gracefulStopRequested = false;
143
+ this._gracefulStopReason = null;
144
+ this._idleWaiters = [];
142
145
  }
143
146
 
144
147
  rebuildExcludeMatchers() {
@@ -183,6 +186,46 @@ export class CodebaseIndexer {
183
186
  }
184
187
  }
185
188
 
189
+ isBusy() {
190
+ return Boolean(this.isIndexing || this.processingWatchEvents);
191
+ }
192
+
193
+ requestGracefulStop(reason = 'unknown') {
194
+ const normalizedReason = String(reason || 'unknown');
195
+ if (this._gracefulStopRequested && this._gracefulStopReason === normalizedReason) return;
196
+ this._gracefulStopRequested = true;
197
+ this._gracefulStopReason = normalizedReason;
198
+ console.info(`[Indexer] Graceful stop requested (${normalizedReason}).`);
199
+ }
200
+
201
+ waitForIdle(timeoutMs = 0) {
202
+ if (!this.isBusy()) {
203
+ return Promise.resolve({ idle: true, timedOut: false });
204
+ }
205
+
206
+ return new Promise((resolve) => {
207
+ const waiter = { resolve, timer: null };
208
+ if (Number.isFinite(timeoutMs) && timeoutMs >= 0) {
209
+ waiter.timer = setTimeout(() => {
210
+ const idx = this._idleWaiters.indexOf(waiter);
211
+ if (idx >= 0) this._idleWaiters.splice(idx, 1);
212
+ resolve({ idle: false, timedOut: true });
213
+ }, timeoutMs);
214
+ waiter.timer.unref?.();
215
+ }
216
+ this._idleWaiters.push(waiter);
217
+ });
218
+ }
219
+
220
+ notifyIdleWaiters() {
221
+ if (this.isBusy() || this._idleWaiters.length === 0) return;
222
+ const pending = this._idleWaiters.splice(0, this._idleWaiters.length);
223
+ for (const waiter of pending) {
224
+ if (waiter.timer) clearTimeout(waiter.timer);
225
+ waiter.resolve({ idle: true, timedOut: false });
226
+ }
227
+ }
228
+
186
229
  getEmbeddingChildRestartThresholdMb() {
187
230
  const totalMb = typeof os.totalmem === 'function' ? os.totalmem() / 1024 / 1024 : 8192;
188
231
  if (this.isHeavyEmbeddingModel()) {
@@ -2131,6 +2174,8 @@ export class CodebaseIndexer {
2131
2174
  }
2132
2175
 
2133
2176
  this.isIndexing = true;
2177
+ this._gracefulStopRequested = false;
2178
+ this._gracefulStopReason = null;
2134
2179
  let memoryTimer = null;
2135
2180
  const logMemory = (label) => {
2136
2181
  if (!this.config.verbose) return;
@@ -2276,6 +2321,20 @@ export class CodebaseIndexer {
2276
2321
  ? 'full'
2277
2322
  : 'incremental';
2278
2323
  this.currentIndexMode = indexMode;
2324
+ const missingHashCount = filesToProcess.reduce(
2325
+ (count, entry) => count + (entry?.expectedHash ? 0 : 1),
2326
+ 0
2327
+ );
2328
+ const indexReason = force
2329
+ ? 'full'
2330
+ : this.cache.getVectorStore().length === 0
2331
+ ? 'initial'
2332
+ : missingHashCount > 0
2333
+ ? 'resume-partial'
2334
+ : 'incremental';
2335
+ console.info(
2336
+ `[Indexer] Index reason: ${indexReason} (changed=${filesToProcess.length}, missingHash=${missingHashCount}, total=${files.length})`
2337
+ );
2279
2338
 
2280
2339
  if (filesToProcess.length === 0) {
2281
2340
  console.info('[Indexer] All files unchanged, nothing to index');
@@ -2370,6 +2429,8 @@ export class CodebaseIndexer {
2370
2429
  const checkpointIntervalMs = this.getIndexCheckpointIntervalMs();
2371
2430
  let lastCheckpointSaveAt = Date.now();
2372
2431
  let checkpointSaveCount = 0;
2432
+ let stoppedEarly = false;
2433
+ let stopCheckpointSaved = false;
2373
2434
 
2374
2435
  console.info(
2375
2436
  `[Indexer] Embedding pass started: ${filesToProcess.length} files using ${this.config.embeddingModel}`
@@ -2731,6 +2792,26 @@ export class CodebaseIndexer {
2731
2792
  );
2732
2793
  }
2733
2794
 
2795
+ if (this._gracefulStopRequested && processedFiles < filesToProcess.length) {
2796
+ const stopReason = this._gracefulStopReason || 'unknown';
2797
+ console.info(
2798
+ `[Indexer] Graceful stop acknowledged at batch boundary (${stopReason}); saving checkpoint...`
2799
+ );
2800
+ try {
2801
+ await this.traceIncrementalMemoryPhase('indexAll.shutdownCheckpointSave', async () => {
2802
+ await this.cache.save({ throwOnError: true });
2803
+ });
2804
+ checkpointSaveCount += 1;
2805
+ lastCheckpointSaveAt = Date.now();
2806
+ stopCheckpointSaved = true;
2807
+ } catch (error) {
2808
+ const message = error instanceof Error ? error.message : String(error);
2809
+ console.error(`[Indexer] Graceful stop checkpoint save failed: ${message}`);
2810
+ }
2811
+ stoppedEarly = true;
2812
+ break;
2813
+ }
2814
+
2734
2815
  allChunks.length = 0;
2735
2816
  filesForWorkers.length = 0;
2736
2817
  fileStats.clear();
@@ -2746,9 +2827,15 @@ export class CodebaseIndexer {
2746
2827
 
2747
2828
  const totalDurationMs = Date.now() - totalStartTime;
2748
2829
  const totalTime = (totalDurationMs / 1000).toFixed(1);
2749
- console.info(
2750
- `[Indexer] Embedding pass complete: ${totalChunks} chunks from ${filesToProcess.length} files in ${totalTime}s`
2751
- );
2830
+ if (stoppedEarly) {
2831
+ console.info(
2832
+ `[Indexer] Embedding pass stopped early: ${totalChunks} chunks from ${processedFiles}/${filesToProcess.length} files in ${totalTime}s`
2833
+ );
2834
+ } else {
2835
+ console.info(
2836
+ `[Indexer] Embedding pass complete: ${totalChunks} chunks from ${filesToProcess.length} files in ${totalTime}s`
2837
+ );
2838
+ }
2752
2839
 
2753
2840
  this.sendProgress(
2754
2841
  95,
@@ -2761,13 +2848,16 @@ export class CodebaseIndexer {
2761
2848
  lastIndexStartedAt: indexStartedAt,
2762
2849
  lastIndexEndedAt: new Date().toISOString(),
2763
2850
  lastDiscoveredFiles: files.length,
2764
- lastFilesProcessed: filesToProcess.length,
2851
+ lastFilesProcessed: processedFiles,
2765
2852
  lastIndexMode: indexMode,
2853
+ lastIndexReason: indexReason,
2766
2854
  lastBatchSize: adaptiveBatchSize,
2767
2855
  lastWorkerThreads: resolvedWorkerThreads,
2768
2856
  lastEmbeddingProcessPerBatch: useEmbeddingProcessPerBatch,
2769
2857
  lastCheckpointIntervalMs: checkpointIntervalMs,
2770
2858
  lastCheckpointSaves: checkpointSaveCount,
2859
+ lastStoppedEarly: stoppedEarly,
2860
+ lastShutdownCheckpointSaved: stopCheckpointSaved,
2771
2861
  });
2772
2862
  try {
2773
2863
  await this.cache.save({ throwOnError: true });
@@ -2787,44 +2877,51 @@ export class CodebaseIndexer {
2787
2877
  const totalFiles = new Set(vectorStoreSnapshot.map((v) => v.file)).size;
2788
2878
  const totalChunksCount = vectorStoreSnapshot.length;
2789
2879
 
2790
- if (this.config.clearCacheAfterIndex) {
2791
- console.info(
2792
- '[Indexer] clearCacheAfterIndex enabled; in-memory vectors will be reloaded on next query'
2793
- );
2794
- await this.cache.dropInMemoryVectors();
2795
- if (this.config.verbose) {
2796
- console.info('[Cache] Cleared in-memory vectors after indexing');
2880
+ if (stoppedEarly) {
2881
+ console.info('[Indexer] Skipping post-index cleanup due graceful stop.');
2882
+ } else {
2883
+ if (this.config.clearCacheAfterIndex) {
2884
+ console.info(
2885
+ '[Indexer] clearCacheAfterIndex enabled; in-memory vectors will be reloaded on next query'
2886
+ );
2887
+ await this.cache.dropInMemoryVectors();
2888
+ if (this.config.verbose) {
2889
+ console.info('[Cache] Cleared in-memory vectors after indexing');
2890
+ }
2797
2891
  }
2798
- }
2799
2892
 
2800
- if (this.config.unloadModelAfterIndex) {
2801
- console.info(
2802
- '[Indexer] unloadModelAfterIndex enabled; embedding model will be reloaded on next query'
2803
- );
2804
- await this.unloadEmbeddingModels();
2805
- }
2806
- this.maybeShutdownQueryEmbeddingPool('full index');
2893
+ if (this.config.unloadModelAfterIndex) {
2894
+ console.info(
2895
+ '[Indexer] unloadModelAfterIndex enabled; embedding model will be reloaded on next query'
2896
+ );
2897
+ await this.unloadEmbeddingModels();
2898
+ }
2899
+ this.maybeShutdownQueryEmbeddingPool('full index');
2807
2900
 
2808
- if (this.config.callGraphEnabled) {
2809
- this.cache.rebuildCallGraph();
2810
- }
2901
+ if (this.config.callGraphEnabled) {
2902
+ this.cache.rebuildCallGraph();
2903
+ }
2811
2904
 
2812
- if (!this.config.clearCacheAfterIndex) {
2813
- void this.cache.ensureAnnIndex().catch((error) => {
2814
- if (this.config.verbose) {
2815
- console.warn(`[ANN] Background ANN build failed: ${error.message}`);
2816
- }
2817
- });
2905
+ if (!this.config.clearCacheAfterIndex) {
2906
+ void this.cache.ensureAnnIndex().catch((error) => {
2907
+ if (this.config.verbose) {
2908
+ console.warn(`[ANN] Background ANN build failed: ${error.message}`);
2909
+ }
2910
+ });
2911
+ }
2818
2912
  }
2819
2913
 
2820
2914
  return {
2821
2915
  skipped: false,
2822
- filesProcessed: filesToProcess.length,
2916
+ filesProcessed: processedFiles,
2823
2917
  chunksCreated: totalChunks,
2824
2918
  totalFiles,
2825
2919
  totalChunks: totalChunksCount,
2826
2920
  duration: totalTime,
2827
- message: `Indexed ${filesToProcess.length} files (${totalChunks} chunks) in ${totalTime}s`,
2921
+ stoppedEarly,
2922
+ message: stoppedEarly
2923
+ ? `Stopped after ${processedFiles}/${filesToProcess.length} files (${totalChunks} chunks) in ${totalTime}s`
2924
+ : `Indexed ${filesToProcess.length} files (${totalChunks} chunks) in ${totalTime}s`,
2828
2925
  };
2829
2926
  } finally {
2830
2927
  if (memoryTimer) {
@@ -2840,6 +2937,7 @@ export class CodebaseIndexer {
2840
2937
  } catch (error) {
2841
2938
  console.warn(`[Indexer] Failed to apply queued file updates: ${error.message}`);
2842
2939
  }
2940
+ this.notifyIdleWaiters();
2843
2941
  }
2844
2942
  }
2845
2943
 
@@ -2906,6 +3004,7 @@ export class CodebaseIndexer {
2906
3004
  }
2907
3005
  } finally {
2908
3006
  this.processingWatchEvents = false;
3007
+ this.notifyIdleWaiters();
2909
3008
  }
2910
3009
  }
2911
3010
 
@@ -1,13 +1,43 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import { acquireWorkspaceLock, releaseWorkspaceLock } from '../lib/server-lifecycle.js';
4
- import { getWorkspaceCachePath } from '../lib/workspace-cache-key.js';
4
+ import {
5
+ getWorkspaceCachePath,
6
+ getWorkspaceCachePathCandidates,
7
+ } from '../lib/workspace-cache-key.js';
5
8
  import { cleanupStaleBinaryArtifacts } from '../lib/vector-store-binary.js';
6
9
 
7
10
  function getWorkspaceCacheDir(workspacePath, globalCacheDir) {
8
11
  return getWorkspaceCachePath(workspacePath, globalCacheDir);
9
12
  }
10
13
 
14
+ async function pathExists(targetPath) {
15
+ try {
16
+ await fs.access(targetPath);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ async function resolveWorkspaceCacheDir(workspacePath, globalCacheDir) {
24
+ const candidates = getWorkspaceCachePathCandidates(workspacePath, globalCacheDir);
25
+
26
+ if (await pathExists(candidates.canonical)) {
27
+ return { cacheDirectory: candidates.canonical, mode: 'canonical' };
28
+ }
29
+ if (
30
+ candidates.compatDriveCase !== candidates.canonical &&
31
+ (await pathExists(candidates.compatDriveCase))
32
+ ) {
33
+ return { cacheDirectory: candidates.compatDriveCase, mode: 'compat-drivecase' };
34
+ }
35
+ if (candidates.legacy !== candidates.canonical && (await pathExists(candidates.legacy))) {
36
+ return { cacheDirectory: candidates.legacy, mode: 'legacy' };
37
+ }
38
+ return { cacheDirectory: candidates.canonical, mode: 'canonical' };
39
+ }
40
+
11
41
  export function getToolDefinition() {
12
42
  return {
13
43
  name: 'f_set_workspace',
@@ -77,7 +107,11 @@ export class SetWorkspaceFeature {
77
107
  this.config.searchDirectory = normalizedPath;
78
108
 
79
109
  const globalCacheDir = this.getGlobalCacheDir();
80
- let newCacheDir = getWorkspaceCacheDir(normalizedPath, globalCacheDir);
110
+ const cacheResolution = await resolveWorkspaceCacheDir(normalizedPath, globalCacheDir);
111
+ let newCacheDir = cacheResolution.cacheDirectory;
112
+ if (this.config.verbose || cacheResolution.mode !== 'canonical') {
113
+ console.info(`[SetWorkspace] Cache resolution mode: ${cacheResolution.mode}`);
114
+ }
81
115
 
82
116
  const legacyPath = path.join(normalizedPath, '.smart-coding-cache');
83
117
  try {