@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.
@@ -25,17 +25,28 @@ export class AnnConfigTool {
25
25
  return this.cache.setEfSearch(efSearch);
26
26
  }
27
27
 
28
- if (action === 'rebuild') {
29
-
30
- this.cache.invalidateAnnIndex();
31
- const index = await this.cache.ensureAnnIndex();
32
- return {
33
- success: index !== null,
34
- message: index
35
- ? 'ANN index rebuilt successfully'
36
- : 'ANN index rebuild failed or not available',
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
- 100,
2867
- 100,
2868
- `Complete: ${totalChunks} chunks from ${filesToProcess.length} files in ${totalTime}s`
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;
@@ -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) {
@@ -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
- args: [],
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
- await this.cache.load();
192
- } catch (err) {
193
- console.warn(`[SetWorkspace] Failed to load cache: ${err.message}`);
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 { CodebaseIndexer } from './features/index-codebase.js';
54
- import { HybridSearch } from './features/hybrid-search.js';
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
- cache = new EmbeddingsCache(config);
835
- console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
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 tryAutoAttachWorkspaceCache = async (reason) => {
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
- JSON_WORKER_THRESHOLD_BYTES,
10
- ANN_DIMENSION_SAMPLE_SIZE,
11
- HNSWLIB_ERROR_RESET_MS,
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
- console.warn(`[Cache] Failed to load binary vector store: ${err.message}`);
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
- console.warn('[Cache] Failed to save cache:', error.message);
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;