@softerist/heuristic-mcp 3.2.0 → 3.2.2
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/config.jsonc +6 -17
- package/features/ann-config.js +26 -15
- package/features/index-codebase.js +17 -11
- package/features/lifecycle.js +53 -9
- package/features/register.js +11 -11
- package/features/set-workspace.js +11 -7
- package/index.js +45 -17
- package/lib/cache.js +60 -43
- package/lib/vector-store-binary.js +361 -43
- package/package.json +1 -1
package/config.jsonc
CHANGED
|
@@ -628,7 +628,7 @@
|
|
|
628
628
|
"**/.smart-coding-cache/**",
|
|
629
629
|
],
|
|
630
630
|
// Indexing controls.
|
|
631
|
-
"indexing": {
|
|
631
|
+
"indexing": {
|
|
632
632
|
// Enable project-type detection + smart ignore patterns.
|
|
633
633
|
"smartIndexing": true,
|
|
634
634
|
// Lines per chunk.
|
|
@@ -645,18 +645,10 @@
|
|
|
645
645
|
"watchFiles": true,
|
|
646
646
|
// Save incremental index checkpoints every 2s so interrupted runs can resume with minimal rework.
|
|
647
647
|
// Increase to 5000-10000 on slower disks if checkpoint writes feel too frequent.
|
|
648
|
-
"indexCheckpointIntervalMs": 2000,
|
|
649
|
-
},
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
// This helps prevent stale workspace binding after IDE reloads/window switches.
|
|
653
|
-
"autoStopOtherServersOnStartup": true,
|
|
654
|
-
// Safety guard for IDE sessions:
|
|
655
|
-
// when true, semantic/index tools require a current trusted workspace signal
|
|
656
|
-
// (MCP roots or explicit workspace env), otherwise the call fails fast.
|
|
657
|
-
"requireTrustedWorkspaceSignalForTools": true,
|
|
658
|
-
// Logging and diagnostics.
|
|
659
|
-
"logging": {
|
|
648
|
+
"indexCheckpointIntervalMs": 2000,
|
|
649
|
+
},
|
|
650
|
+
// Logging and diagnostics.
|
|
651
|
+
"logging": {
|
|
660
652
|
// Enable verbose logging.
|
|
661
653
|
"verbose": true,
|
|
662
654
|
},
|
|
@@ -694,10 +686,7 @@
|
|
|
694
686
|
"worker": {
|
|
695
687
|
// Number of embedding workers (0 disables).
|
|
696
688
|
// Windows + heavy Jina models are more stable with child-process embedding than worker pools.
|
|
697
|
-
"workerThreads":
|
|
698
|
-
// Safety guard for heavy models on Windows.
|
|
699
|
-
// Keep true (recommended). Set false only to opt in to experimental heavy-model workers on Windows.
|
|
700
|
-
"workerDisableHeavyModelOnWindows": false,
|
|
689
|
+
"workerThreads": 0,
|
|
701
690
|
// Worker batch timeout in milliseconds.
|
|
702
691
|
"workerBatchTimeoutMs": 120000,
|
|
703
692
|
// Failures before worker circuit opens.
|
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,35 @@ 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
|
+
}
|
|
1273
|
+
|
|
1230
1274
|
if (metaData && isProgressIncomplete) {
|
|
1231
1275
|
console.info(' Indexing state: Cached snapshot available; incremental update running.');
|
|
1232
1276
|
} 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 } 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);
|
|
@@ -883,6 +893,9 @@ async function initialize(workspaceDir) {
|
|
|
883
893
|
config.searchDirectory = candidate.workspace;
|
|
884
894
|
config.cacheDirectory = candidate.cacheDirectory;
|
|
885
895
|
await fs.mkdir(config.cacheDirectory, { recursive: true });
|
|
896
|
+
if (config.vectorStoreFormat === 'binary') {
|
|
897
|
+
await cleanupStaleBinaryArtifacts(config.cacheDirectory, { logger: console });
|
|
898
|
+
}
|
|
886
899
|
await cache.load();
|
|
887
900
|
console.info(
|
|
888
901
|
`[Server] Auto-attached workspace cache (${reason}): ${candidate.workspace} via ${candidate.source}`
|
|
@@ -1162,19 +1175,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1162
1175
|
for (const feature of features) {
|
|
1163
1176
|
const toolDef = feature.module.getToolDefinition(config);
|
|
1164
1177
|
|
|
1165
|
-
if (request.params.name === toolDef.name) {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (typeof feature.handler !== 'function') {
|
|
1169
|
-
return {
|
|
1178
|
+
if (request.params.name === toolDef.name) {
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
if (typeof feature.handler !== 'function') {
|
|
1182
|
+
return {
|
|
1170
1183
|
content: [{
|
|
1171
1184
|
type: 'text',
|
|
1172
1185
|
text: `Tool "${toolDef.name}" is not ready. Server may still be initializing.`,
|
|
1173
1186
|
}],
|
|
1174
|
-
isError: true,
|
|
1175
|
-
};
|
|
1187
|
+
isError: true,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
let result;
|
|
1191
|
+
try {
|
|
1192
|
+
result = await feature.handler(request, feature.instance);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1195
|
+
console.error(`[Server] Tool ${toolDef.name} failed: ${message}`);
|
|
1196
|
+
return {
|
|
1197
|
+
content: [
|
|
1198
|
+
{
|
|
1199
|
+
type: 'text',
|
|
1200
|
+
text: `Error: ${message || 'Unknown tool failure'}`,
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
isError: true,
|
|
1204
|
+
};
|
|
1176
1205
|
}
|
|
1177
|
-
const result = await feature.handler(request, feature.instance);
|
|
1178
1206
|
if (toolDef.name === 'f_set_workspace' && !isToolResponseError(result)) {
|
|
1179
1207
|
trustWorkspacePath(config.searchDirectory);
|
|
1180
1208
|
}
|
package/lib/cache.js
CHANGED
|
@@ -266,10 +266,12 @@ export class EmbeddingsCache {
|
|
|
266
266
|
};
|
|
267
267
|
|
|
268
268
|
|
|
269
|
-
this.saveQueue = Promise.resolve();
|
|
270
|
-
this._saveTimer = null;
|
|
271
|
-
this._saveRequested = false;
|
|
272
|
-
this._savePromise = null;
|
|
269
|
+
this.saveQueue = Promise.resolve();
|
|
270
|
+
this._saveTimer = null;
|
|
271
|
+
this._saveRequested = false;
|
|
272
|
+
this._savePromise = null;
|
|
273
|
+
this._saveThrowOnError = false;
|
|
274
|
+
this.lastSaveError = null;
|
|
273
275
|
|
|
274
276
|
|
|
275
277
|
this.annIndex = null;
|
|
@@ -771,31 +773,39 @@ export class EmbeddingsCache {
|
|
|
771
773
|
|
|
772
774
|
|
|
773
775
|
|
|
774
|
-
save() {
|
|
775
|
-
if (!this.config.enableCache) return Promise.resolve();
|
|
776
|
-
|
|
777
|
-
this._saveRequested = true;
|
|
778
|
-
|
|
779
|
-
|
|
776
|
+
save({ throwOnError = false } = {}) {
|
|
777
|
+
if (!this.config.enableCache) return Promise.resolve();
|
|
778
|
+
|
|
779
|
+
this._saveRequested = true;
|
|
780
|
+
if (throwOnError) {
|
|
781
|
+
this._saveThrowOnError = true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (this._saveTimer) return this._savePromise ?? Promise.resolve();
|
|
780
785
|
|
|
781
786
|
const debounceMs = Number.isInteger(this.config.saveDebounceMs)
|
|
782
787
|
? this.config.saveDebounceMs
|
|
783
788
|
: 250;
|
|
784
789
|
|
|
785
|
-
this._savePromise = new Promise((resolve, reject) => {
|
|
786
|
-
this._saveTimer = setTimeout(() => {
|
|
787
|
-
this._saveTimer = null;
|
|
788
|
-
|
|
789
|
-
this.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
790
|
+
this._savePromise = new Promise((resolve, reject) => {
|
|
791
|
+
this._saveTimer = setTimeout(() => {
|
|
792
|
+
this._saveTimer = null;
|
|
793
|
+
const rejectOnSaveError = this._saveThrowOnError;
|
|
794
|
+
this._saveThrowOnError = false;
|
|
795
|
+
|
|
796
|
+
this.saveQueue = this.saveQueue
|
|
797
|
+
.catch(() => {
|
|
798
|
+
|
|
799
|
+
})
|
|
800
|
+
.then(async () => {
|
|
801
|
+
while (this._saveRequested) {
|
|
802
|
+
this._saveRequested = false;
|
|
803
|
+
await this.performSave({ throwOnError: rejectOnSaveError });
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
.then(resolve, reject)
|
|
807
|
+
.finally(() => {
|
|
808
|
+
this._savePromise = null;
|
|
799
809
|
});
|
|
800
810
|
}, debounceMs);
|
|
801
811
|
});
|
|
@@ -803,7 +813,7 @@ export class EmbeddingsCache {
|
|
|
803
813
|
return this._savePromise;
|
|
804
814
|
}
|
|
805
815
|
|
|
806
|
-
async performSave() {
|
|
816
|
+
async performSave({ throwOnError = false } = {}) {
|
|
807
817
|
|
|
808
818
|
this._saveInProgress = true;
|
|
809
819
|
if (
|
|
@@ -988,9 +998,9 @@ export class EmbeddingsCache {
|
|
|
988
998
|
|
|
989
999
|
|
|
990
1000
|
|
|
991
|
-
if (
|
|
992
|
-
this.config.annIndexCache !== false &&
|
|
993
|
-
this.annPersistDirty &&
|
|
1001
|
+
if (
|
|
1002
|
+
this.config.annIndexCache !== false &&
|
|
1003
|
+
this.annPersistDirty &&
|
|
994
1004
|
!this.annDirty &&
|
|
995
1005
|
!this._annWriting &&
|
|
996
1006
|
this.annIndex &&
|
|
@@ -1009,10 +1019,12 @@ export class EmbeddingsCache {
|
|
|
1009
1019
|
console.warn(`[ANN] Failed to persist ANN index: ${error.message}`);
|
|
1010
1020
|
} finally {
|
|
1011
1021
|
this._annWriting = false;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
this.lastSaveError = null;
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
this.lastSaveError = error instanceof Error ? error : new Error(String(error));
|
|
1027
|
+
console.warn('[Cache] Failed to save cache:', this.lastSaveError.message);
|
|
1016
1028
|
|
|
1017
1029
|
if (
|
|
1018
1030
|
this.config.vectorStoreFormat === 'binary' &&
|
|
@@ -1031,9 +1043,9 @@ export class EmbeddingsCache {
|
|
|
1031
1043
|
}
|
|
1032
1044
|
}
|
|
1033
1045
|
|
|
1034
|
-
if (
|
|
1035
|
-
this.config.vectorStoreFormat === 'sqlite' &&
|
|
1036
|
-
!this.sqliteStore
|
|
1046
|
+
if (
|
|
1047
|
+
this.config.vectorStoreFormat === 'sqlite' &&
|
|
1048
|
+
!this.sqliteStore
|
|
1037
1049
|
) {
|
|
1038
1050
|
try {
|
|
1039
1051
|
console.info('[Cache] Attempting to recover SQLite store after failed save...');
|
|
@@ -1043,14 +1055,19 @@ export class EmbeddingsCache {
|
|
|
1043
1055
|
}
|
|
1044
1056
|
} catch (recoverErr) {
|
|
1045
1057
|
console.warn(`[Cache] Failed to recover SQLite store: ${recoverErr.message}`);
|
|
1046
|
-
this.sqliteStore = null;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1058
|
+
this.sqliteStore = null;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (throwOnError) {
|
|
1062
|
+
const wrapped = new Error(`Cache save failed: ${this.lastSaveError.message}`);
|
|
1063
|
+
wrapped.cause = this.lastSaveError;
|
|
1064
|
+
throw wrapped;
|
|
1065
|
+
}
|
|
1066
|
+
} finally {
|
|
1067
|
+
this.isSaving = false;
|
|
1068
|
+
this._saveInProgress = false;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1054
1071
|
|
|
1055
1072
|
|
|
1056
1073
|
|
|
@@ -14,30 +14,344 @@ const MAGIC_VECTORS = 'HMCV';
|
|
|
14
14
|
const MAGIC_RECORDS = 'HMCR';
|
|
15
15
|
const MAGIC_CONTENT = 'HMCC';
|
|
16
16
|
|
|
17
|
-
const VECTORS_FILE = 'vectors.bin';
|
|
18
|
-
const RECORDS_FILE = 'records.bin';
|
|
19
|
-
const CONTENT_FILE = 'content.bin';
|
|
20
|
-
const FILES_FILE = 'files.json';
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
17
|
+
const VECTORS_FILE = 'vectors.bin';
|
|
18
|
+
const RECORDS_FILE = 'records.bin';
|
|
19
|
+
const CONTENT_FILE = 'content.bin';
|
|
20
|
+
const FILES_FILE = 'files.json';
|
|
21
|
+
const TELEMETRY_FILE = 'binary-store-telemetry.json';
|
|
22
|
+
const RETRYABLE_RENAME_ERRORS = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
23
|
+
const BINARY_ARTIFACT_BASE_FILES = [VECTORS_FILE, RECORDS_FILE, CONTENT_FILE, FILES_FILE];
|
|
24
|
+
const STARTUP_TMP_CLEANUP_MIN_AGE_MS = 2 * 60 * 1000;
|
|
25
|
+
const TELEMETRY_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
function createTelemetryTotals() {
|
|
28
|
+
return {
|
|
29
|
+
atomicReplaceAttempts: 0,
|
|
30
|
+
atomicReplaceSuccesses: 0,
|
|
31
|
+
atomicReplaceFailures: 0,
|
|
32
|
+
renameRetryCount: 0,
|
|
33
|
+
fallbackCopyCount: 0,
|
|
34
|
+
rollbackCount: 0,
|
|
35
|
+
rollbackRestoreFailureCount: 0,
|
|
36
|
+
startupCleanupRuns: 0,
|
|
37
|
+
staleTempFilesRemoved: 0,
|
|
38
|
+
staleTempFilesSkippedActive: 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeTelemetry(raw) {
|
|
43
|
+
const totals = createTelemetryTotals();
|
|
44
|
+
if (raw?.totals && typeof raw.totals === 'object') {
|
|
45
|
+
for (const key of Object.keys(totals)) {
|
|
46
|
+
if (Number.isFinite(raw.totals[key])) {
|
|
47
|
+
totals[key] = raw.totals[key];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
version: TELEMETRY_VERSION,
|
|
53
|
+
totals,
|
|
54
|
+
updatedAt: typeof raw?.updatedAt === 'string' ? raw.updatedAt : null,
|
|
55
|
+
lastError:
|
|
56
|
+
raw?.lastError && typeof raw.lastError === 'object'
|
|
57
|
+
? {
|
|
58
|
+
at: typeof raw.lastError.at === 'string' ? raw.lastError.at : null,
|
|
59
|
+
message:
|
|
60
|
+
typeof raw.lastError.message === 'string' ? raw.lastError.message : null,
|
|
61
|
+
}
|
|
62
|
+
: null,
|
|
63
|
+
lastAtomicReplace:
|
|
64
|
+
raw?.lastAtomicReplace && typeof raw.lastAtomicReplace === 'object'
|
|
65
|
+
? { ...raw.lastAtomicReplace }
|
|
66
|
+
: null,
|
|
67
|
+
lastStartupCleanup:
|
|
68
|
+
raw?.lastStartupCleanup && typeof raw.lastStartupCleanup === 'object'
|
|
69
|
+
? { ...raw.lastStartupCleanup }
|
|
70
|
+
: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readTelemetryFile(cacheDir) {
|
|
75
|
+
const telemetryPath = path.join(cacheDir, TELEMETRY_FILE);
|
|
76
|
+
try {
|
|
77
|
+
const raw = await fs.readFile(telemetryPath, 'utf-8');
|
|
78
|
+
return normalizeTelemetry(JSON.parse(raw));
|
|
79
|
+
} catch {
|
|
80
|
+
return normalizeTelemetry(null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function writeTelemetryFile(cacheDir, telemetry) {
|
|
85
|
+
const telemetryPath = path.join(cacheDir, TELEMETRY_FILE);
|
|
86
|
+
await fs.mkdir(cacheDir, { recursive: true }).catch(() => {});
|
|
87
|
+
await fs.writeFile(telemetryPath, JSON.stringify(telemetry, null, 2));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function updateTelemetry(cacheDir, mutate) {
|
|
91
|
+
if (!cacheDir) return;
|
|
92
|
+
try {
|
|
93
|
+
const telemetry = await readTelemetryFile(cacheDir);
|
|
94
|
+
mutate(telemetry);
|
|
95
|
+
telemetry.updatedAt = new Date().toISOString();
|
|
96
|
+
await writeTelemetryFile(cacheDir, telemetry);
|
|
97
|
+
} catch {
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isProcessRunning(pid) {
|
|
103
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 0);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return err?.code === 'EPERM';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parsePidFromBinaryArtifact(fileName) {
|
|
113
|
+
const match = fileName.match(/\.(?:tmp|bak)-(\d+)(?:-|$)/);
|
|
114
|
+
if (!match) return null;
|
|
115
|
+
const pid = Number.parseInt(match[1], 10);
|
|
116
|
+
return Number.isInteger(pid) ? pid : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isBinaryTempArtifact(fileName) {
|
|
120
|
+
return BINARY_ARTIFACT_BASE_FILES.some(
|
|
121
|
+
(baseFile) =>
|
|
122
|
+
fileName.startsWith(`${baseFile}.tmp-`) || fileName.startsWith(`${baseFile}.bak-`)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function addToMetric(metrics, key, value = 1) {
|
|
127
|
+
if (!metrics || !Number.isFinite(value) || value <= 0) return;
|
|
128
|
+
metrics[key] = (metrics[key] || 0) + value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function readBinaryStoreTelemetry(cacheDir) {
|
|
132
|
+
return readTelemetryFile(cacheDir);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function cleanupStaleBinaryArtifacts(
|
|
136
|
+
cacheDir,
|
|
137
|
+
{ minAgeMs = STARTUP_TMP_CLEANUP_MIN_AGE_MS, logger = null } = {}
|
|
138
|
+
) {
|
|
139
|
+
const result = {
|
|
140
|
+
cacheDir,
|
|
141
|
+
scanned: 0,
|
|
142
|
+
removed: 0,
|
|
143
|
+
skippedActive: 0,
|
|
144
|
+
removedFiles: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let entries = [];
|
|
148
|
+
try {
|
|
149
|
+
entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
150
|
+
} catch {
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const fileName = typeof entry === 'string' ? entry : entry?.name;
|
|
157
|
+
if (!fileName) continue;
|
|
158
|
+
const isFileEntry = typeof entry === 'string' ? true : entry?.isFile?.() === true;
|
|
159
|
+
if (!isFileEntry) continue;
|
|
160
|
+
if (!isBinaryTempArtifact(fileName)) continue;
|
|
161
|
+
result.scanned += 1;
|
|
162
|
+
|
|
163
|
+
const fullPath = path.join(cacheDir, fileName);
|
|
164
|
+
const stats = await fs.stat(fullPath).catch(() => null);
|
|
165
|
+
if (!stats) continue;
|
|
166
|
+
|
|
167
|
+
const ageMs = now - stats.mtimeMs;
|
|
168
|
+
const ownerPid = parsePidFromBinaryArtifact(fileName);
|
|
169
|
+
if (ownerPid && isProcessRunning(ownerPid)) {
|
|
170
|
+
result.skippedActive += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ageMs < minAgeMs) continue;
|
|
174
|
+
|
|
175
|
+
await fs.rm(fullPath, { force: true }).catch(() => {});
|
|
176
|
+
result.removed += 1;
|
|
177
|
+
result.removedFiles.push(fileName);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await updateTelemetry(cacheDir, (telemetry) => {
|
|
181
|
+
telemetry.totals.startupCleanupRuns += 1;
|
|
182
|
+
telemetry.totals.staleTempFilesRemoved += result.removed;
|
|
183
|
+
telemetry.totals.staleTempFilesSkippedActive += result.skippedActive;
|
|
184
|
+
telemetry.lastStartupCleanup = {
|
|
185
|
+
at: new Date().toISOString(),
|
|
186
|
+
scanned: result.scanned,
|
|
187
|
+
removed: result.removed,
|
|
188
|
+
skippedActive: result.skippedActive,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (logger && result.removed > 0) {
|
|
193
|
+
logger.info(
|
|
194
|
+
`[Cache] Startup temp cleanup removed ${result.removed} stale artifact(s) from ${cacheDir}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isRetryableRenameError(err) {
|
|
202
|
+
return RETRYABLE_RENAME_ERRORS.has(err?.code);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function renameWithRetry(
|
|
206
|
+
source,
|
|
207
|
+
target,
|
|
208
|
+
{ retries = 12, delayMs = 50, maxDelayMs = 1000 } = {}
|
|
209
|
+
) {
|
|
210
|
+
let attempt = 0;
|
|
211
|
+
let delay = delayMs;
|
|
212
|
+
while (true) {
|
|
213
|
+
try {
|
|
214
|
+
await fs.rename(source, target);
|
|
215
|
+
return attempt;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (!isRetryableRenameError(err) || attempt >= retries) {
|
|
218
|
+
err.renameRetryCount = attempt;
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
222
|
+
attempt += 1;
|
|
223
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function pathExists(filePath) {
|
|
229
|
+
try {
|
|
230
|
+
await fs.access(filePath);
|
|
231
|
+
return true;
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function removeIfExists(filePath) {
|
|
238
|
+
await fs.rm(filePath, { force: true }).catch(() => {});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function promoteFileWithFallback(source, target, renameOptions = {}, metrics = null) {
|
|
242
|
+
try {
|
|
243
|
+
const retriesUsed = await renameWithRetry(source, target, renameOptions);
|
|
244
|
+
addToMetric(metrics, 'renameRetryCount', retriesUsed);
|
|
245
|
+
return;
|
|
246
|
+
} catch (renameError) {
|
|
247
|
+
const retriesUsed = Number.isFinite(renameError?.renameRetryCount)
|
|
248
|
+
? renameError.renameRetryCount
|
|
249
|
+
: 0;
|
|
250
|
+
addToMetric(metrics, 'renameRetryCount', retriesUsed);
|
|
251
|
+
if (!isRetryableRenameError(renameError)) {
|
|
252
|
+
throw renameError;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await fs.copyFile(source, target);
|
|
257
|
+
await removeIfExists(source);
|
|
258
|
+
addToMetric(metrics, 'fallbackCopyCount', 1);
|
|
259
|
+
return;
|
|
260
|
+
} catch (copyError) {
|
|
261
|
+
const wrapped = new Error(
|
|
262
|
+
`rename failed (${renameError.message}); fallback copy failed (${copyError.message})`
|
|
263
|
+
);
|
|
264
|
+
wrapped.code = copyError?.code || renameError?.code;
|
|
265
|
+
throw wrapped;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function replaceFilesAtomically(filePairs, renameOptions = {}) {
|
|
271
|
+
const metrics = createTelemetryTotals();
|
|
272
|
+
metrics.atomicReplaceAttempts = 1;
|
|
273
|
+
const cacheDir = filePairs.length > 0 ? path.dirname(filePairs[0].target) : null;
|
|
274
|
+
const backupSuffix = `.bak-${process.pid}-${Date.now()}`;
|
|
275
|
+
const backups = [];
|
|
276
|
+
const replacedTargets = [];
|
|
277
|
+
let operationError = null;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// Stage current files as backups first. If this fails, nothing is replaced.
|
|
281
|
+
for (const pair of filePairs) {
|
|
282
|
+
if (!(await pathExists(pair.target))) continue;
|
|
283
|
+
const backupPath = `${pair.target}${backupSuffix}`;
|
|
284
|
+
await removeIfExists(backupPath);
|
|
285
|
+
await promoteFileWithFallback(pair.target, backupPath, renameOptions, metrics);
|
|
286
|
+
backups.push({ target: pair.target, backupPath });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Replace targets with new temp files.
|
|
290
|
+
for (const pair of filePairs) {
|
|
291
|
+
await promoteFileWithFallback(pair.source, pair.target, renameOptions, metrics);
|
|
292
|
+
replacedTargets.push(pair.target);
|
|
293
|
+
}
|
|
294
|
+
metrics.atomicReplaceSuccesses = 1;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
operationError = error;
|
|
297
|
+
metrics.atomicReplaceFailures = 1;
|
|
298
|
+
metrics.rollbackCount = 1;
|
|
299
|
+
const rollbackErrors = [];
|
|
300
|
+
|
|
301
|
+
// Remove any partially replaced files before restoring backups.
|
|
302
|
+
for (const target of replacedTargets.reverse()) {
|
|
303
|
+
await removeIfExists(target);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Restore original files from backups.
|
|
307
|
+
for (const backup of backups.reverse()) {
|
|
308
|
+
try {
|
|
309
|
+
await promoteFileWithFallback(backup.backupPath, backup.target, renameOptions, metrics);
|
|
310
|
+
} catch (restoreErr) {
|
|
311
|
+
rollbackErrors.push(
|
|
312
|
+
`restore ${path.basename(backup.target)} failed: ${restoreErr.message}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (rollbackErrors.length > 0) {
|
|
317
|
+
metrics.rollbackRestoreFailureCount = rollbackErrors.length;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Clean up temp files left from this failed write attempt.
|
|
321
|
+
await Promise.all(filePairs.map((pair) => removeIfExists(pair.source)));
|
|
322
|
+
|
|
323
|
+
if (rollbackErrors.length > 0) {
|
|
324
|
+
error.message = `${error.message}. Rollback issues: ${rollbackErrors.join('; ')}`;
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
} finally {
|
|
328
|
+
// Best-effort cleanup for any backup remnants after success/rollback.
|
|
329
|
+
await Promise.all(backups.map((backup) => removeIfExists(backup.backupPath)));
|
|
330
|
+
await updateTelemetry(cacheDir, (telemetry) => {
|
|
331
|
+
telemetry.totals.atomicReplaceAttempts += metrics.atomicReplaceAttempts;
|
|
332
|
+
telemetry.totals.atomicReplaceSuccesses += metrics.atomicReplaceSuccesses;
|
|
333
|
+
telemetry.totals.atomicReplaceFailures += metrics.atomicReplaceFailures;
|
|
334
|
+
telemetry.totals.renameRetryCount += metrics.renameRetryCount;
|
|
335
|
+
telemetry.totals.fallbackCopyCount += metrics.fallbackCopyCount;
|
|
336
|
+
telemetry.totals.rollbackCount += metrics.rollbackCount;
|
|
337
|
+
telemetry.totals.rollbackRestoreFailureCount += metrics.rollbackRestoreFailureCount;
|
|
338
|
+
telemetry.lastAtomicReplace = {
|
|
339
|
+
at: new Date().toISOString(),
|
|
340
|
+
success: metrics.atomicReplaceSuccesses > 0,
|
|
341
|
+
renameRetryCount: metrics.renameRetryCount,
|
|
342
|
+
fallbackCopyCount: metrics.fallbackCopyCount,
|
|
343
|
+
rollbackCount: metrics.rollbackCount,
|
|
344
|
+
rollbackRestoreFailureCount: metrics.rollbackRestoreFailureCount,
|
|
345
|
+
};
|
|
346
|
+
if (operationError) {
|
|
347
|
+
telemetry.lastError = {
|
|
348
|
+
at: new Date().toISOString(),
|
|
349
|
+
message: operationError.message,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
41
355
|
|
|
42
356
|
function writeMagic(buffer, magic) {
|
|
43
357
|
buffer.write(magic, 0, 'ascii');
|
|
@@ -433,18 +747,19 @@ export class BinaryVectorStore {
|
|
|
433
747
|
return map;
|
|
434
748
|
}
|
|
435
749
|
|
|
436
|
-
static async write(
|
|
437
|
-
cacheDir,
|
|
438
|
-
chunks,
|
|
439
|
-
{
|
|
750
|
+
static async write(
|
|
751
|
+
cacheDir,
|
|
752
|
+
chunks,
|
|
753
|
+
{
|
|
440
754
|
contentCacheEntries,
|
|
441
755
|
vectorCacheEntries,
|
|
442
|
-
vectorLoadMode,
|
|
443
|
-
getContent,
|
|
444
|
-
getVector,
|
|
445
|
-
preRename,
|
|
446
|
-
|
|
447
|
-
|
|
756
|
+
vectorLoadMode,
|
|
757
|
+
getContent,
|
|
758
|
+
getVector,
|
|
759
|
+
preRename,
|
|
760
|
+
renameOptions,
|
|
761
|
+
} = {}
|
|
762
|
+
) {
|
|
448
763
|
ensureLittleEndian();
|
|
449
764
|
const { vectorsPath, recordsPath, contentPath, filesPath } =
|
|
450
765
|
BinaryVectorStore.getPaths(cacheDir);
|
|
@@ -611,15 +926,18 @@ export class BinaryVectorStore {
|
|
|
611
926
|
await preRename();
|
|
612
927
|
}
|
|
613
928
|
|
|
614
|
-
await
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
929
|
+
await replaceFilesAtomically(
|
|
930
|
+
[
|
|
931
|
+
{ source: vectorsTmp, target: vectorsPath },
|
|
932
|
+
{ source: recordsTmp, target: recordsPath },
|
|
933
|
+
{ source: contentTmp, target: contentPath },
|
|
934
|
+
{ source: filesTmp, target: filesPath },
|
|
935
|
+
],
|
|
936
|
+
renameOptions
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
return BinaryVectorStore.load(cacheDir, {
|
|
940
|
+
contentCacheEntries,
|
|
623
941
|
vectorCacheEntries,
|
|
624
942
|
vectorLoadMode,
|
|
625
943
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softerist/heuristic-mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
4
4
|
"description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|