@softerist/heuristic-mcp 3.2.7 → 3.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/features/index-codebase.js +129 -30
- package/features/set-workspace.js +36 -2
- package/index.js +1702 -1637
- package/lib/config.js +43 -17
- package/lib/workspace-cache-key.js +34 -4
- package/package.json +1 -1
- package/search-configs.js +0 -36
|
@@ -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
|
-
|
|
2750
|
-
|
|
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:
|
|
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 (
|
|
2791
|
-
console.info(
|
|
2792
|
-
|
|
2793
|
-
)
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
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
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
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
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2901
|
+
if (this.config.callGraphEnabled) {
|
|
2902
|
+
this.cache.rebuildCallGraph();
|
|
2903
|
+
}
|
|
2811
2904
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
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:
|
|
2916
|
+
filesProcessed: processedFiles,
|
|
2823
2917
|
chunksCreated: totalChunks,
|
|
2824
2918
|
totalFiles,
|
|
2825
2919
|
totalChunks: totalChunksCount,
|
|
2826
2920
|
duration: totalTime,
|
|
2827
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|