@softerist/heuristic-mcp 3.2.1 → 3.2.3
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/ann-config.js +26 -15
- package/features/index-codebase.js +17 -11
- package/features/lifecycle.js +83 -9
- package/features/register.js +11 -11
- package/features/set-workspace.js +11 -7
- package/index.js +95 -22
- package/lib/cache.js +79 -30
- package/lib/constants.js +8 -7
- package/lib/vector-store-binary.js +599 -43
- package/package.json +1 -1
package/features/ann-config.js
CHANGED
|
@@ -25,17 +25,28 @@ export class AnnConfigTool {
|
|
|
25
25
|
return this.cache.setEfSearch(efSearch);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (action === 'rebuild') {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
if (action === 'rebuild') {
|
|
29
|
+
try {
|
|
30
|
+
|
|
31
|
+
this.cache.invalidateAnnIndex();
|
|
32
|
+
const index = await this.cache.ensureAnnIndex();
|
|
33
|
+
if (!index) {
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
error: 'ANN index rebuild failed or not available',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
message: 'ANN index rebuilt successfully',
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: error instanceof Error ? error.message : String(error),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
39
50
|
|
|
40
51
|
return {
|
|
41
52
|
success: false,
|
|
@@ -43,10 +54,10 @@ export class AnnConfigTool {
|
|
|
43
54
|
};
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
formatResults(result) {
|
|
47
|
-
if (result.success === false) {
|
|
48
|
-
return `Error: ${result.error}`;
|
|
49
|
-
}
|
|
57
|
+
formatResults(result) {
|
|
58
|
+
if (result.success === false) {
|
|
59
|
+
return `Error: ${result.error || result.message || 'Unknown error'}`;
|
|
60
|
+
}
|
|
50
61
|
|
|
51
62
|
if (result.enabled !== undefined) {
|
|
52
63
|
|
|
@@ -2857,16 +2857,16 @@ export class CodebaseIndexer {
|
|
|
2857
2857
|
|
|
2858
2858
|
const totalDurationMs = Date.now() - totalStartTime;
|
|
2859
2859
|
const totalTime = (totalDurationMs / 1000).toFixed(1);
|
|
2860
|
-
console.info(
|
|
2861
|
-
`[Indexer] Embedding pass complete: ${totalChunks} chunks from ${filesToProcess.length} files in ${totalTime}s`
|
|
2862
|
-
);
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
this.sendProgress(
|
|
2866
|
-
|
|
2867
|
-
100,
|
|
2868
|
-
`
|
|
2869
|
-
);
|
|
2860
|
+
console.info(
|
|
2861
|
+
`[Indexer] Embedding pass complete: ${totalChunks} chunks from ${filesToProcess.length} files in ${totalTime}s`
|
|
2862
|
+
);
|
|
2863
|
+
|
|
2864
|
+
|
|
2865
|
+
this.sendProgress(
|
|
2866
|
+
95,
|
|
2867
|
+
100,
|
|
2868
|
+
`Embedding complete; saving cache (${totalChunks} chunks from ${filesToProcess.length} files)...`
|
|
2869
|
+
);
|
|
2870
2870
|
|
|
2871
2871
|
this.cache.setLastIndexDuration?.(totalDurationMs);
|
|
2872
2872
|
this.cache.setLastIndexStats?.({
|
|
@@ -2881,7 +2881,13 @@ export class CodebaseIndexer {
|
|
|
2881
2881
|
lastCheckpointIntervalMs: checkpointIntervalMs,
|
|
2882
2882
|
lastCheckpointSaves: checkpointSaveCount,
|
|
2883
2883
|
});
|
|
2884
|
-
await this.cache.save();
|
|
2884
|
+
await this.cache.save({ throwOnError: true });
|
|
2885
|
+
|
|
2886
|
+
this.sendProgress(
|
|
2887
|
+
100,
|
|
2888
|
+
100,
|
|
2889
|
+
`Complete: ${totalChunks} chunks from ${filesToProcess.length} files in ${totalTime}s`
|
|
2890
|
+
);
|
|
2885
2891
|
|
|
2886
2892
|
const vectorStoreSnapshot = this.cache.getVectorStore();
|
|
2887
2893
|
const totalFiles = new Set(vectorStoreSnapshot.map((v) => v.file)).size;
|
package/features/lifecycle.js
CHANGED
|
@@ -14,8 +14,23 @@ import {
|
|
|
14
14
|
upsertMcpServerEntryInText,
|
|
15
15
|
} from '../lib/settings-editor.js';
|
|
16
16
|
|
|
17
|
-
const execPromise = util.promisify(exec);
|
|
18
|
-
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
17
|
+
const execPromise = util.promisify(exec);
|
|
18
|
+
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
19
|
+
const BINARY_TELEMETRY_FILE = 'binary-store-telemetry.json';
|
|
20
|
+
|
|
21
|
+
async function readBinaryTelemetry(cacheDir) {
|
|
22
|
+
const telemetryPath = path.join(cacheDir, BINARY_TELEMETRY_FILE);
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(await fs.readFile(telemetryPath, 'utf-8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasNonZeroBinaryTelemetry(totals) {
|
|
31
|
+
if (!totals || typeof totals !== 'object') return false;
|
|
32
|
+
return Object.values(totals).some((value) => Number.isFinite(value) && value > 0);
|
|
33
|
+
}
|
|
19
34
|
|
|
20
35
|
function getUserHomeDir() {
|
|
21
36
|
if (process.platform === 'win32' && process.env.USERPROFILE) {
|
|
@@ -1208,18 +1223,18 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
|
|
|
1208
1223
|
if (progressData.indexMode) {
|
|
1209
1224
|
console.info(` Current index mode: ${String(progressData.indexMode)}`);
|
|
1210
1225
|
}
|
|
1211
|
-
if (
|
|
1212
|
-
progressData.workerCircuitOpen &&
|
|
1213
|
-
Number.isFinite(progressData.workersDisabledUntil)
|
|
1214
|
-
) {
|
|
1226
|
+
if (
|
|
1227
|
+
progressData.workerCircuitOpen &&
|
|
1228
|
+
Number.isFinite(progressData.workersDisabledUntil)
|
|
1229
|
+
) {
|
|
1215
1230
|
const remainingMs = progressData.workersDisabledUntil - Date.now();
|
|
1216
1231
|
const remainingLabel = formatDurationMs(Math.max(0, remainingMs));
|
|
1217
1232
|
console.info(` Workers paused: ${remainingLabel || '0s'} remaining`);
|
|
1218
1233
|
console.info(
|
|
1219
1234
|
` Workers disabled until: ${formatDateTime(progressData.workersDisabledUntil)}`
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
} else {
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1223
1238
|
if (metaData) {
|
|
1224
1239
|
console.info(' Summary: Cached snapshot available; no update running.');
|
|
1225
1240
|
} else {
|
|
@@ -1227,6 +1242,65 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
|
|
|
1227
1242
|
}
|
|
1228
1243
|
}
|
|
1229
1244
|
|
|
1245
|
+
const binaryTelemetry = await readBinaryTelemetry(cacheDir);
|
|
1246
|
+
if (binaryTelemetry?.totals && hasNonZeroBinaryTelemetry(binaryTelemetry.totals)) {
|
|
1247
|
+
const totals = binaryTelemetry.totals;
|
|
1248
|
+
console.info(
|
|
1249
|
+
` Binary telemetry: swaps=${totals.atomicReplaceAttempts || 0} ok=${totals.atomicReplaceSuccesses || 0} fail=${totals.atomicReplaceFailures || 0}`
|
|
1250
|
+
);
|
|
1251
|
+
console.info(
|
|
1252
|
+
` Binary telemetry: retries=${totals.renameRetryCount || 0} fallbackCopies=${totals.fallbackCopyCount || 0} rollbacks=${totals.rollbackCount || 0}`
|
|
1253
|
+
);
|
|
1254
|
+
if ((totals.rollbackRestoreFailureCount || 0) > 0) {
|
|
1255
|
+
console.info(
|
|
1256
|
+
` Binary telemetry: rollback restore failures=${totals.rollbackRestoreFailureCount}`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
if ((totals.startupCleanupRuns || 0) > 0 || (totals.staleTempFilesRemoved || 0) > 0) {
|
|
1260
|
+
console.info(
|
|
1261
|
+
` Startup temp cleanup: runs=${totals.startupCleanupRuns || 0} removed=${totals.staleTempFilesRemoved || 0} skippedActive=${totals.staleTempFilesSkippedActive || 0}`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
if (binaryTelemetry.lastAtomicReplace?.at) {
|
|
1265
|
+
console.info(
|
|
1266
|
+
` Last atomic replace: ${formatDateTime(binaryTelemetry.lastAtomicReplace.at)}`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (binaryTelemetry.lastError?.message) {
|
|
1270
|
+
console.info(` Last binary error: ${binaryTelemetry.lastError.message}`);
|
|
1271
|
+
}
|
|
1272
|
+
if (
|
|
1273
|
+
(totals.corruptionDetected || 0) > 0 ||
|
|
1274
|
+
(totals.corruptionAutoCleared || 0) > 0 ||
|
|
1275
|
+
(totals.corruptionSecondaryReadonlyBlocked || 0) > 0
|
|
1276
|
+
) {
|
|
1277
|
+
console.info(
|
|
1278
|
+
` Corruption telemetry: detected=${totals.corruptionDetected || 0} autoCleared=${totals.corruptionAutoCleared || 0} secondaryBlocked=${totals.corruptionSecondaryReadonlyBlocked || 0}`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
if (binaryTelemetry.lastCorruption?.at || binaryTelemetry.lastCorruption?.message) {
|
|
1282
|
+
const atLabel = binaryTelemetry.lastCorruption?.at
|
|
1283
|
+
? formatDateTime(binaryTelemetry.lastCorruption.at)
|
|
1284
|
+
: 'unknown time';
|
|
1285
|
+
const actionLabel =
|
|
1286
|
+
typeof binaryTelemetry.lastCorruption?.action === 'string'
|
|
1287
|
+
? binaryTelemetry.lastCorruption.action
|
|
1288
|
+
: 'unknown';
|
|
1289
|
+
const contextLabel =
|
|
1290
|
+
typeof binaryTelemetry.lastCorruption?.context === 'string'
|
|
1291
|
+
? binaryTelemetry.lastCorruption.context
|
|
1292
|
+
: 'n/a';
|
|
1293
|
+
const msgLabel =
|
|
1294
|
+
typeof binaryTelemetry.lastCorruption?.message === 'string' &&
|
|
1295
|
+
binaryTelemetry.lastCorruption.message.trim().length > 0
|
|
1296
|
+
? ` message=${binaryTelemetry.lastCorruption.message}`
|
|
1297
|
+
: '';
|
|
1298
|
+
console.info(
|
|
1299
|
+
` Last corruption event: ${atLabel} action=${actionLabel} context=${contextLabel}${msgLabel}`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1230
1304
|
if (metaData && isProgressIncomplete) {
|
|
1231
1305
|
console.info(' Indexing state: Cached snapshot available; incremental update running.');
|
|
1232
1306
|
} else if (metaData) {
|
package/features/register.js
CHANGED
|
@@ -265,22 +265,22 @@ function ideMatchesFilter(name, filter) {
|
|
|
265
265
|
|
|
266
266
|
function getServerConfigForIde(name) {
|
|
267
267
|
const normalizedName = normalizeIdeName(name);
|
|
268
|
+
const workspaceArgs = [
|
|
269
|
+
'--workspace',
|
|
270
|
+
'${workspaceFolder}',
|
|
271
|
+
'--workspace',
|
|
272
|
+
'${workspaceRoot}',
|
|
273
|
+
'--workspace',
|
|
274
|
+
'${workspace}',
|
|
275
|
+
];
|
|
268
276
|
const config = {
|
|
269
277
|
command: 'heuristic-mcp',
|
|
270
|
-
|
|
278
|
+
// Prefer explicit workspace forwarding when supported by the host.
|
|
279
|
+
// Unexpanded placeholders are safely ignored by CLI workspace parsing.
|
|
280
|
+
args: workspaceArgs,
|
|
271
281
|
};
|
|
272
282
|
|
|
273
283
|
if (normalizedName === 'antigravity') {
|
|
274
|
-
// Prefer explicit workspace forwarding in VS Code-compatible clients.
|
|
275
|
-
// If the variable is not expanded by the IDE, CLI parsing safely ignores it.
|
|
276
|
-
config.args = [
|
|
277
|
-
'--workspace',
|
|
278
|
-
'${workspaceFolder}',
|
|
279
|
-
'--workspace',
|
|
280
|
-
'${workspaceRoot}',
|
|
281
|
-
'--workspace',
|
|
282
|
-
'${workspace}',
|
|
283
|
-
];
|
|
284
284
|
// Allow provider-specific workspace env discovery as a backup signal.
|
|
285
285
|
config.env = {
|
|
286
286
|
HEURISTIC_MCP_ENABLE_DYNAMIC_WORKSPACE_ENV: 'true',
|
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
5
|
import { acquireWorkspaceLock, releaseWorkspaceLock } from '../lib/server-lifecycle.js';
|
|
6
6
|
import { getWorkspaceCachePath } from '../lib/workspace-cache-key.js';
|
|
7
|
+
import { cleanupStaleBinaryArtifacts } from '../lib/vector-store-binary.js';
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
function getWorkspaceCacheDir(workspacePath, globalCacheDir) {
|
|
@@ -185,13 +186,16 @@ export class SetWorkspaceFeature {
|
|
|
185
186
|
await releaseWorkspaceLock({ cacheDirectory: previousCache });
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
if (this.cache && typeof this.cache.load === 'function') {
|
|
190
|
-
try {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
|
|
190
|
+
if (this.cache && typeof this.cache.load === 'function') {
|
|
191
|
+
try {
|
|
192
|
+
if (this.config.vectorStoreFormat === 'binary') {
|
|
193
|
+
await cleanupStaleBinaryArtifacts(newCacheDir);
|
|
194
|
+
}
|
|
195
|
+
await this.cache.load();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.warn(`[SetWorkspace] Failed to load cache: ${err.message}`);
|
|
198
|
+
}
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
|
package/index.js
CHANGED
|
@@ -49,9 +49,10 @@ import {
|
|
|
49
49
|
stopOtherHeuristicServers,
|
|
50
50
|
} from './lib/server-lifecycle.js';
|
|
51
51
|
|
|
52
|
-
import { EmbeddingsCache } from './lib/cache.js';
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
52
|
+
import { EmbeddingsCache } from './lib/cache.js';
|
|
53
|
+
import { cleanupStaleBinaryArtifacts, recordBinaryStoreCorruption } from './lib/vector-store-binary.js';
|
|
54
|
+
import { CodebaseIndexer } from './features/index-codebase.js';
|
|
55
|
+
import { HybridSearch } from './features/hybrid-search.js';
|
|
55
56
|
|
|
56
57
|
import * as IndexCodebaseFeature from './features/index-codebase.js';
|
|
57
58
|
import * as HybridSearchFeature from './features/hybrid-search.js';
|
|
@@ -827,12 +828,21 @@ async function initialize(workspaceDir) {
|
|
|
827
828
|
}
|
|
828
829
|
};
|
|
829
830
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
if (config.vectorStoreFormat === 'binary') {
|
|
836
|
+
try {
|
|
837
|
+
await cleanupStaleBinaryArtifacts(config.cacheDirectory, { logger: console });
|
|
838
|
+
} catch (err) {
|
|
839
|
+
console.warn(`[Cache] Startup temp cleanup failed: ${err.message}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
cache = new EmbeddingsCache(config);
|
|
845
|
+
console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
|
|
836
846
|
|
|
837
847
|
|
|
838
848
|
indexer = new CodebaseIndexer(embedder, cache, config, server);
|
|
@@ -869,7 +879,25 @@ async function initialize(workspaceDir) {
|
|
|
869
879
|
stopStartupMemory();
|
|
870
880
|
}
|
|
871
881
|
};
|
|
872
|
-
const
|
|
882
|
+
const handleCorruptCacheAfterLoad = async ({ context, canReindex }) => {
|
|
883
|
+
if (!cache.consumeAutoReindex()) return false;
|
|
884
|
+
cache.clearInMemoryState();
|
|
885
|
+
await recordBinaryStoreCorruption(config.cacheDirectory, {
|
|
886
|
+
context,
|
|
887
|
+
action: canReindex ? 'auto-cleared' : 'secondary-readonly-blocked',
|
|
888
|
+
});
|
|
889
|
+
if (canReindex) {
|
|
890
|
+
console.warn(
|
|
891
|
+
`[Server] Cache corruption detected while ${context}; in-memory cache was cleared and a full re-index will run.`
|
|
892
|
+
);
|
|
893
|
+
} else {
|
|
894
|
+
console.warn(
|
|
895
|
+
`[Server] Cache corruption detected while ${context}. This server is secondary read-only and cannot re-index. Reload the IDE window for this workspace or use the primary instance to rebuild the cache.`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
};
|
|
900
|
+
const tryAutoAttachWorkspaceCache = async (reason, { canReindex = workspaceLockAcquired } = {}) => {
|
|
873
901
|
const candidate = await findAutoAttachWorkspaceCandidate({
|
|
874
902
|
excludeCacheDirectory: config.cacheDirectory,
|
|
875
903
|
});
|
|
@@ -883,7 +911,14 @@ async function initialize(workspaceDir) {
|
|
|
883
911
|
config.searchDirectory = candidate.workspace;
|
|
884
912
|
config.cacheDirectory = candidate.cacheDirectory;
|
|
885
913
|
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
914
|
+
if (config.vectorStoreFormat === 'binary') {
|
|
915
|
+
await cleanupStaleBinaryArtifacts(config.cacheDirectory, { logger: console });
|
|
916
|
+
}
|
|
886
917
|
await cache.load();
|
|
918
|
+
await handleCorruptCacheAfterLoad({
|
|
919
|
+
context: `auto-attaching workspace cache (${reason})`,
|
|
920
|
+
canReindex,
|
|
921
|
+
});
|
|
887
922
|
console.info(
|
|
888
923
|
`[Server] Auto-attached workspace cache (${reason}): ${candidate.workspace} via ${candidate.source}`
|
|
889
924
|
);
|
|
@@ -903,7 +938,9 @@ async function initialize(workspaceDir) {
|
|
|
903
938
|
console.warn(
|
|
904
939
|
`[Server] Detected system fallback workspace: ${config.searchDirectory}. Attempting cache auto-attach.`
|
|
905
940
|
);
|
|
906
|
-
const attached = await tryAutoAttachWorkspaceCache('system-fallback'
|
|
941
|
+
const attached = await tryAutoAttachWorkspaceCache('system-fallback', {
|
|
942
|
+
canReindex: workspaceLockAcquired,
|
|
943
|
+
});
|
|
907
944
|
if (!attached) {
|
|
908
945
|
console.warn(
|
|
909
946
|
'[Server] Waiting for a proper workspace root (MCP roots, env vars, or f_set_workspace).'
|
|
@@ -919,8 +956,12 @@ async function initialize(workspaceDir) {
|
|
|
919
956
|
try {
|
|
920
957
|
console.info('[Server] Secondary instance detected; loading cache in read-only mode.');
|
|
921
958
|
await cache.load();
|
|
959
|
+
await handleCorruptCacheAfterLoad({
|
|
960
|
+
context: 'loading cache in secondary read-only mode',
|
|
961
|
+
canReindex: false,
|
|
962
|
+
});
|
|
922
963
|
if (cache.getStoreSize() === 0) {
|
|
923
|
-
await tryAutoAttachWorkspaceCache('secondary-empty-cache');
|
|
964
|
+
await tryAutoAttachWorkspaceCache('secondary-empty-cache', { canReindex: false });
|
|
924
965
|
}
|
|
925
966
|
if (config.verbose) {
|
|
926
967
|
logMemory('[Server] Memory (after cache load)');
|
|
@@ -934,9 +975,10 @@ async function initialize(workspaceDir) {
|
|
|
934
975
|
|
|
935
976
|
void preloadEmbeddingModel();
|
|
936
977
|
|
|
937
|
-
try {
|
|
938
|
-
console.info('[Server] Loading cache (deferred)...');
|
|
978
|
+
try {
|
|
979
|
+
console.info('[Server] Loading cache (deferred)...');
|
|
939
980
|
await cache.load();
|
|
981
|
+
await handleCorruptCacheAfterLoad({ context: 'startup cache load', canReindex: true });
|
|
940
982
|
if (config.verbose) {
|
|
941
983
|
logMemory('[Server] Memory (after cache load)');
|
|
942
984
|
}
|
|
@@ -1092,6 +1134,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1092
1134
|
try {
|
|
1093
1135
|
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
1094
1136
|
await cache.load();
|
|
1137
|
+
if (cache.consumeAutoReindex()) {
|
|
1138
|
+
cache.clearInMemoryState();
|
|
1139
|
+
await recordBinaryStoreCorruption(config.cacheDirectory, {
|
|
1140
|
+
context: 'f_set_workspace read-only attach',
|
|
1141
|
+
action: 'secondary-readonly-blocked',
|
|
1142
|
+
});
|
|
1143
|
+
return {
|
|
1144
|
+
content: [
|
|
1145
|
+
{
|
|
1146
|
+
type: 'text',
|
|
1147
|
+
text: `Attached cache for ${normalizedPath}, but it is corrupt. This secondary read-only instance cannot rebuild it. Reload the IDE window for this workspace or run indexing from the primary instance.`,
|
|
1148
|
+
},
|
|
1149
|
+
],
|
|
1150
|
+
isError: true,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1095
1153
|
trustWorkspacePath(normalizedPath);
|
|
1096
1154
|
return {
|
|
1097
1155
|
content: [
|
|
@@ -1162,19 +1220,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1162
1220
|
for (const feature of features) {
|
|
1163
1221
|
const toolDef = feature.module.getToolDefinition(config);
|
|
1164
1222
|
|
|
1165
|
-
if (request.params.name === toolDef.name) {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (typeof feature.handler !== 'function') {
|
|
1169
|
-
return {
|
|
1223
|
+
if (request.params.name === toolDef.name) {
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
if (typeof feature.handler !== 'function') {
|
|
1227
|
+
return {
|
|
1170
1228
|
content: [{
|
|
1171
1229
|
type: 'text',
|
|
1172
1230
|
text: `Tool "${toolDef.name}" is not ready. Server may still be initializing.`,
|
|
1173
1231
|
}],
|
|
1174
|
-
isError: true,
|
|
1175
|
-
};
|
|
1232
|
+
isError: true,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
let result;
|
|
1236
|
+
try {
|
|
1237
|
+
result = await feature.handler(request, feature.instance);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1240
|
+
console.error(`[Server] Tool ${toolDef.name} failed: ${message}`);
|
|
1241
|
+
return {
|
|
1242
|
+
content: [
|
|
1243
|
+
{
|
|
1244
|
+
type: 'text',
|
|
1245
|
+
text: `Error: ${message || 'Unknown tool failure'}`,
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
isError: true,
|
|
1249
|
+
};
|
|
1176
1250
|
}
|
|
1177
|
-
const result = await feature.handler(request, feature.instance);
|
|
1178
1251
|
if (toolDef.name === 'f_set_workspace' && !isToolResponseError(result)) {
|
|
1179
1252
|
trustWorkspacePath(config.searchDirectory);
|
|
1180
1253
|
}
|
package/lib/cache.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { Worker } from 'worker_threads';
|
|
4
|
-
import { StreamingJsonWriter } from './json-writer.js';
|
|
5
|
-
import { BinaryVectorStore } from './vector-store-binary.js';
|
|
6
|
-
import { SqliteVectorStore } from './vector-store-sqlite.js';
|
|
7
|
-
import { isNonProjectDirectory } from './config.js';
|
|
3
|
+
import { Worker } from 'worker_threads';
|
|
4
|
+
import { StreamingJsonWriter } from './json-writer.js';
|
|
8
5
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
BinaryVectorStore,
|
|
7
|
+
BinaryStoreCorruptionError,
|
|
8
|
+
recordBinaryStoreCorruption,
|
|
9
|
+
} from './vector-store-binary.js';
|
|
10
|
+
import { SqliteVectorStore } from './vector-store-sqlite.js';
|
|
11
|
+
import { isNonProjectDirectory } from './config.js';
|
|
12
|
+
import {
|
|
13
|
+
JSON_WORKER_THRESHOLD_BYTES,
|
|
14
|
+
ANN_DIMENSION_SAMPLE_SIZE,
|
|
15
|
+
HNSWLIB_ERROR_RESET_MS,
|
|
12
16
|
DEFAULT_READER_WAIT_TIMEOUT_MS,
|
|
13
17
|
} from './constants.js';
|
|
14
18
|
|
|
@@ -270,6 +274,8 @@ export class EmbeddingsCache {
|
|
|
270
274
|
this._saveTimer = null;
|
|
271
275
|
this._saveRequested = false;
|
|
272
276
|
this._savePromise = null;
|
|
277
|
+
this._saveThrowOnError = false;
|
|
278
|
+
this.lastSaveError = null;
|
|
273
279
|
|
|
274
280
|
|
|
275
281
|
this.annIndex = null;
|
|
@@ -301,8 +307,23 @@ export class EmbeddingsCache {
|
|
|
301
307
|
|
|
302
308
|
this._clearedAfterIndex = false;
|
|
303
309
|
this._loadPromise = null;
|
|
310
|
+
this._corruptionDetected = false;
|
|
304
311
|
}
|
|
305
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Returns true if the last load() detected binary store corruption.
|
|
315
|
+
* Used by the server to decide whether to trigger an automatic re-index.
|
|
316
|
+
*/
|
|
317
|
+
shouldAutoReindex() {
|
|
318
|
+
return this._corruptionDetected === true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
consumeAutoReindex() {
|
|
322
|
+
const should = this._corruptionDetected === true;
|
|
323
|
+
this._corruptionDetected = false;
|
|
324
|
+
return should;
|
|
325
|
+
}
|
|
326
|
+
|
|
306
327
|
|
|
307
328
|
addInitError(stage, error) {
|
|
308
329
|
this.initErrors.push({
|
|
@@ -508,6 +529,7 @@ export class EmbeddingsCache {
|
|
|
508
529
|
|
|
509
530
|
async load({ forceVectorLoadMode } = {}) {
|
|
510
531
|
if (!this.config.enableCache) return;
|
|
532
|
+
this._corruptionDetected = false;
|
|
511
533
|
|
|
512
534
|
try {
|
|
513
535
|
await fs.mkdir(this.config.cacheDirectory, { recursive: true });
|
|
@@ -658,7 +680,19 @@ export class EmbeddingsCache {
|
|
|
658
680
|
});
|
|
659
681
|
} catch (err) {
|
|
660
682
|
this.binaryStore = null;
|
|
661
|
-
|
|
683
|
+
const isCorruption = err instanceof BinaryStoreCorruptionError ||
|
|
684
|
+
err?.name === 'BinaryStoreCorruptionError';
|
|
685
|
+
if (isCorruption) {
|
|
686
|
+
console.warn(`[Cache] Binary store corruption detected: ${err.message}`);
|
|
687
|
+
this._corruptionDetected = true;
|
|
688
|
+
await recordBinaryStoreCorruption(this.config.cacheDirectory, {
|
|
689
|
+
message: err.message,
|
|
690
|
+
context: 'cache.load binary store',
|
|
691
|
+
action: 'detected',
|
|
692
|
+
});
|
|
693
|
+
} else {
|
|
694
|
+
console.warn(`[Cache] Failed to load binary vector store: ${err.message}`);
|
|
695
|
+
}
|
|
662
696
|
}
|
|
663
697
|
}
|
|
664
698
|
|
|
@@ -771,10 +805,13 @@ export class EmbeddingsCache {
|
|
|
771
805
|
|
|
772
806
|
|
|
773
807
|
|
|
774
|
-
save() {
|
|
808
|
+
save({ throwOnError = false } = {}) {
|
|
775
809
|
if (!this.config.enableCache) return Promise.resolve();
|
|
776
810
|
|
|
777
811
|
this._saveRequested = true;
|
|
812
|
+
if (throwOnError) {
|
|
813
|
+
this._saveThrowOnError = true;
|
|
814
|
+
}
|
|
778
815
|
|
|
779
816
|
if (this._saveTimer) return this._savePromise ?? Promise.resolve();
|
|
780
817
|
|
|
@@ -785,12 +822,17 @@ export class EmbeddingsCache {
|
|
|
785
822
|
this._savePromise = new Promise((resolve, reject) => {
|
|
786
823
|
this._saveTimer = setTimeout(() => {
|
|
787
824
|
this._saveTimer = null;
|
|
825
|
+
const rejectOnSaveError = this._saveThrowOnError;
|
|
826
|
+
this._saveThrowOnError = false;
|
|
788
827
|
|
|
789
828
|
this.saveQueue = this.saveQueue
|
|
829
|
+
.catch(() => {
|
|
830
|
+
|
|
831
|
+
})
|
|
790
832
|
.then(async () => {
|
|
791
833
|
while (this._saveRequested) {
|
|
792
834
|
this._saveRequested = false;
|
|
793
|
-
await this.performSave();
|
|
835
|
+
await this.performSave({ throwOnError: rejectOnSaveError });
|
|
794
836
|
}
|
|
795
837
|
})
|
|
796
838
|
.then(resolve, reject)
|
|
@@ -803,24 +845,24 @@ export class EmbeddingsCache {
|
|
|
803
845
|
return this._savePromise;
|
|
804
846
|
}
|
|
805
847
|
|
|
806
|
-
async performSave() {
|
|
807
|
-
|
|
808
|
-
this._saveInProgress = true;
|
|
809
|
-
if (
|
|
810
|
-
this.config.allowSystemWorkspaceCache !== true &&
|
|
811
|
-
this.config.searchDirectory &&
|
|
812
|
-
isNonProjectDirectory(this.config.searchDirectory)
|
|
813
|
-
) {
|
|
814
|
-
const source = this.config.workspaceResolution?.source || 'unknown';
|
|
815
|
-
console.warn(
|
|
816
|
-
`[Cache] Skipping cache save for non-project workspace (${source}): ${this.config.searchDirectory}`
|
|
817
|
-
);
|
|
818
|
-
this._saveInProgress = false;
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
if (this.activeReads > 0) {
|
|
848
|
+
async performSave({ throwOnError = false } = {}) {
|
|
849
|
+
|
|
850
|
+
this._saveInProgress = true;
|
|
851
|
+
if (
|
|
852
|
+
this.config.allowSystemWorkspaceCache !== true &&
|
|
853
|
+
this.config.searchDirectory &&
|
|
854
|
+
isNonProjectDirectory(this.config.searchDirectory)
|
|
855
|
+
) {
|
|
856
|
+
const source = this.config.workspaceResolution?.source || 'unknown';
|
|
857
|
+
console.warn(
|
|
858
|
+
`[Cache] Skipping cache save for non-project workspace (${source}): ${this.config.searchDirectory}`
|
|
859
|
+
);
|
|
860
|
+
this._saveInProgress = false;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
if (this.activeReads > 0) {
|
|
824
866
|
const timeoutMs = this.config.saveReaderWaitTimeoutMs ?? DEFAULT_READER_WAIT_TIMEOUT_MS;
|
|
825
867
|
const allReadersFinished = await this.waitForReadersWithTimeout(timeoutMs);
|
|
826
868
|
if (!allReadersFinished && !this.config.forceSaveWithActiveReaders) {
|
|
@@ -1011,8 +1053,10 @@ export class EmbeddingsCache {
|
|
|
1011
1053
|
this._annWriting = false;
|
|
1012
1054
|
}
|
|
1013
1055
|
}
|
|
1056
|
+
this.lastSaveError = null;
|
|
1014
1057
|
} catch (error) {
|
|
1015
|
-
|
|
1058
|
+
this.lastSaveError = error instanceof Error ? error : new Error(String(error));
|
|
1059
|
+
console.warn('[Cache] Failed to save cache:', this.lastSaveError.message);
|
|
1016
1060
|
|
|
1017
1061
|
if (
|
|
1018
1062
|
this.config.vectorStoreFormat === 'binary' &&
|
|
@@ -1046,6 +1090,11 @@ export class EmbeddingsCache {
|
|
|
1046
1090
|
this.sqliteStore = null;
|
|
1047
1091
|
}
|
|
1048
1092
|
}
|
|
1093
|
+
if (throwOnError) {
|
|
1094
|
+
const wrapped = new Error(`Cache save failed: ${this.lastSaveError.message}`);
|
|
1095
|
+
wrapped.cause = this.lastSaveError;
|
|
1096
|
+
throw wrapped;
|
|
1097
|
+
}
|
|
1049
1098
|
} finally {
|
|
1050
1099
|
this.isSaving = false;
|
|
1051
1100
|
this._saveInProgress = false;
|