@softerist/heuristic-mcp 3.2.2 → 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.
@@ -1269,6 +1269,36 @@ export async function status({ fix = false, cacheOnly = false, workspaceDir = nu
1269
1269
  if (binaryTelemetry.lastError?.message) {
1270
1270
  console.info(` Last binary error: ${binaryTelemetry.lastError.message}`);
1271
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
+ }
1272
1302
  }
1273
1303
 
1274
1304
  if (metaData && isProgressIncomplete) {
package/index.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  } from './lib/server-lifecycle.js';
51
51
 
52
52
  import { EmbeddingsCache } from './lib/cache.js';
53
- import { cleanupStaleBinaryArtifacts } from './lib/vector-store-binary.js';
53
+ import { cleanupStaleBinaryArtifacts, recordBinaryStoreCorruption } from './lib/vector-store-binary.js';
54
54
  import { CodebaseIndexer } from './features/index-codebase.js';
55
55
  import { HybridSearch } from './features/hybrid-search.js';
56
56
 
@@ -879,7 +879,25 @@ async function initialize(workspaceDir) {
879
879
  stopStartupMemory();
880
880
  }
881
881
  };
882
- 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 } = {}) => {
883
901
  const candidate = await findAutoAttachWorkspaceCandidate({
884
902
  excludeCacheDirectory: config.cacheDirectory,
885
903
  });
@@ -897,6 +915,10 @@ async function initialize(workspaceDir) {
897
915
  await cleanupStaleBinaryArtifacts(config.cacheDirectory, { logger: console });
898
916
  }
899
917
  await cache.load();
918
+ await handleCorruptCacheAfterLoad({
919
+ context: `auto-attaching workspace cache (${reason})`,
920
+ canReindex,
921
+ });
900
922
  console.info(
901
923
  `[Server] Auto-attached workspace cache (${reason}): ${candidate.workspace} via ${candidate.source}`
902
924
  );
@@ -916,7 +938,9 @@ async function initialize(workspaceDir) {
916
938
  console.warn(
917
939
  `[Server] Detected system fallback workspace: ${config.searchDirectory}. Attempting cache auto-attach.`
918
940
  );
919
- const attached = await tryAutoAttachWorkspaceCache('system-fallback');
941
+ const attached = await tryAutoAttachWorkspaceCache('system-fallback', {
942
+ canReindex: workspaceLockAcquired,
943
+ });
920
944
  if (!attached) {
921
945
  console.warn(
922
946
  '[Server] Waiting for a proper workspace root (MCP roots, env vars, or f_set_workspace).'
@@ -932,8 +956,12 @@ async function initialize(workspaceDir) {
932
956
  try {
933
957
  console.info('[Server] Secondary instance detected; loading cache in read-only mode.');
934
958
  await cache.load();
959
+ await handleCorruptCacheAfterLoad({
960
+ context: 'loading cache in secondary read-only mode',
961
+ canReindex: false,
962
+ });
935
963
  if (cache.getStoreSize() === 0) {
936
- await tryAutoAttachWorkspaceCache('secondary-empty-cache');
964
+ await tryAutoAttachWorkspaceCache('secondary-empty-cache', { canReindex: false });
937
965
  }
938
966
  if (config.verbose) {
939
967
  logMemory('[Server] Memory (after cache load)');
@@ -947,9 +975,10 @@ async function initialize(workspaceDir) {
947
975
 
948
976
  void preloadEmbeddingModel();
949
977
 
950
- try {
951
- console.info('[Server] Loading cache (deferred)...');
978
+ try {
979
+ console.info('[Server] Loading cache (deferred)...');
952
980
  await cache.load();
981
+ await handleCorruptCacheAfterLoad({ context: 'startup cache load', canReindex: true });
953
982
  if (config.verbose) {
954
983
  logMemory('[Server] Memory (after cache load)');
955
984
  }
@@ -1105,6 +1134,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1105
1134
  try {
1106
1135
  await fs.mkdir(config.cacheDirectory, { recursive: true });
1107
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
+ }
1108
1153
  trustWorkspacePath(normalizedPath);
1109
1154
  return {
1110
1155
  content: [
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
 
@@ -266,12 +270,12 @@ export class EmbeddingsCache {
266
270
  };
267
271
 
268
272
 
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
+ this.saveQueue = Promise.resolve();
274
+ this._saveTimer = null;
275
+ this._saveRequested = false;
276
+ this._savePromise = null;
277
+ this._saveThrowOnError = false;
278
+ this.lastSaveError = null;
275
279
 
276
280
 
277
281
  this.annIndex = null;
@@ -303,8 +307,23 @@ export class EmbeddingsCache {
303
307
 
304
308
  this._clearedAfterIndex = false;
305
309
  this._loadPromise = null;
310
+ this._corruptionDetected = false;
306
311
  }
307
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
+
308
327
 
309
328
  addInitError(stage, error) {
310
329
  this.initErrors.push({
@@ -510,6 +529,7 @@ export class EmbeddingsCache {
510
529
 
511
530
  async load({ forceVectorLoadMode } = {}) {
512
531
  if (!this.config.enableCache) return;
532
+ this._corruptionDetected = false;
513
533
 
514
534
  try {
515
535
  await fs.mkdir(this.config.cacheDirectory, { recursive: true });
@@ -660,7 +680,19 @@ export class EmbeddingsCache {
660
680
  });
661
681
  } catch (err) {
662
682
  this.binaryStore = null;
663
- 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
+ }
664
696
  }
665
697
  }
666
698
 
@@ -773,39 +805,39 @@ export class EmbeddingsCache {
773
805
 
774
806
 
775
807
 
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();
808
+ save({ throwOnError = false } = {}) {
809
+ if (!this.config.enableCache) return Promise.resolve();
810
+
811
+ this._saveRequested = true;
812
+ if (throwOnError) {
813
+ this._saveThrowOnError = true;
814
+ }
815
+
816
+ if (this._saveTimer) return this._savePromise ?? Promise.resolve();
785
817
 
786
818
  const debounceMs = Number.isInteger(this.config.saveDebounceMs)
787
819
  ? this.config.saveDebounceMs
788
820
  : 250;
789
821
 
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;
822
+ this._savePromise = new Promise((resolve, reject) => {
823
+ this._saveTimer = setTimeout(() => {
824
+ this._saveTimer = null;
825
+ const rejectOnSaveError = this._saveThrowOnError;
826
+ this._saveThrowOnError = false;
827
+
828
+ this.saveQueue = this.saveQueue
829
+ .catch(() => {
830
+
831
+ })
832
+ .then(async () => {
833
+ while (this._saveRequested) {
834
+ this._saveRequested = false;
835
+ await this.performSave({ throwOnError: rejectOnSaveError });
836
+ }
837
+ })
838
+ .then(resolve, reject)
839
+ .finally(() => {
840
+ this._savePromise = null;
809
841
  });
810
842
  }, debounceMs);
811
843
  });
@@ -813,24 +845,24 @@ export class EmbeddingsCache {
813
845
  return this._savePromise;
814
846
  }
815
847
 
816
- async performSave({ throwOnError = false } = {}) {
817
-
818
- this._saveInProgress = true;
819
- if (
820
- this.config.allowSystemWorkspaceCache !== true &&
821
- this.config.searchDirectory &&
822
- isNonProjectDirectory(this.config.searchDirectory)
823
- ) {
824
- const source = this.config.workspaceResolution?.source || 'unknown';
825
- console.warn(
826
- `[Cache] Skipping cache save for non-project workspace (${source}): ${this.config.searchDirectory}`
827
- );
828
- this._saveInProgress = false;
829
- return;
830
- }
831
-
832
-
833
- 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) {
834
866
  const timeoutMs = this.config.saveReaderWaitTimeoutMs ?? DEFAULT_READER_WAIT_TIMEOUT_MS;
835
867
  const allReadersFinished = await this.waitForReadersWithTimeout(timeoutMs);
836
868
  if (!allReadersFinished && !this.config.forceSaveWithActiveReaders) {
@@ -998,9 +1030,9 @@ export class EmbeddingsCache {
998
1030
 
999
1031
 
1000
1032
 
1001
- if (
1002
- this.config.annIndexCache !== false &&
1003
- this.annPersistDirty &&
1033
+ if (
1034
+ this.config.annIndexCache !== false &&
1035
+ this.annPersistDirty &&
1004
1036
  !this.annDirty &&
1005
1037
  !this._annWriting &&
1006
1038
  this.annIndex &&
@@ -1019,12 +1051,12 @@ export class EmbeddingsCache {
1019
1051
  console.warn(`[ANN] Failed to persist ANN index: ${error.message}`);
1020
1052
  } finally {
1021
1053
  this._annWriting = false;
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);
1054
+ }
1055
+ }
1056
+ this.lastSaveError = null;
1057
+ } catch (error) {
1058
+ this.lastSaveError = error instanceof Error ? error : new Error(String(error));
1059
+ console.warn('[Cache] Failed to save cache:', this.lastSaveError.message);
1028
1060
 
1029
1061
  if (
1030
1062
  this.config.vectorStoreFormat === 'binary' &&
@@ -1043,9 +1075,9 @@ export class EmbeddingsCache {
1043
1075
  }
1044
1076
  }
1045
1077
 
1046
- if (
1047
- this.config.vectorStoreFormat === 'sqlite' &&
1048
- !this.sqliteStore
1078
+ if (
1079
+ this.config.vectorStoreFormat === 'sqlite' &&
1080
+ !this.sqliteStore
1049
1081
  ) {
1050
1082
  try {
1051
1083
  console.info('[Cache] Attempting to recover SQLite store after failed save...');
@@ -1055,19 +1087,19 @@ export class EmbeddingsCache {
1055
1087
  }
1056
1088
  } catch (recoverErr) {
1057
1089
  console.warn(`[Cache] Failed to recover SQLite store: ${recoverErr.message}`);
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
- }
1090
+ this.sqliteStore = null;
1091
+ }
1092
+ }
1093
+ if (throwOnError) {
1094
+ const wrapped = new Error(`Cache save failed: ${this.lastSaveError.message}`);
1095
+ wrapped.cause = this.lastSaveError;
1096
+ throw wrapped;
1097
+ }
1098
+ } finally {
1099
+ this.isSaving = false;
1100
+ this._saveInProgress = false;
1101
+ }
1102
+ }
1071
1103
 
1072
1104
 
1073
1105
 
package/lib/constants.js CHANGED
@@ -248,8 +248,9 @@ export const EMBEDDING_PROCESS_GC_STATE_INITIAL = Object.freeze({
248
248
  /**
249
249
  * Binary vector store format version.
250
250
  * Increment when binary format changes to trigger re-indexing.
251
+ * v2: added writeId (4 bytes) + CRC32 (4 bytes) + 8 bytes reserved to all headers.
251
252
  */
252
- export const BINARY_STORE_VERSION = 1;
253
+ export const BINARY_STORE_VERSION = 2;
253
254
 
254
255
  /**
255
256
  * SQLite vector store format version.
@@ -259,21 +260,21 @@ export const SQLITE_STORE_VERSION = 1;
259
260
 
260
261
  /**
261
262
  * Binary vector file header size in bytes.
262
- * Contains: magic (4) + version (4) + dim (4) + count (4) + reserved (4)
263
+ * Contains: magic (4) + version (4) + dim (4) + count (4) + writeId (4) + crc32 (4) + reserved (8)
263
264
  */
264
- export const BINARY_VECTOR_HEADER_SIZE = 20;
265
+ export const BINARY_VECTOR_HEADER_SIZE = 32;
265
266
 
266
267
  /**
267
268
  * Binary record file header size in bytes.
268
- * Contains: magic (4) + version (4) + count (4) + reserved (8)
269
+ * Contains: magic (4) + version (4) + count (4) + fileCount (4) + writeId (4) + crc32 (4) + reserved (8)
269
270
  */
270
- export const BINARY_RECORD_HEADER_SIZE = 20;
271
+ export const BINARY_RECORD_HEADER_SIZE = 32;
271
272
 
272
273
  /**
273
274
  * Binary content file header size in bytes.
274
- * Contains: magic (4) + version (4) + count (4) + reserved (8)
275
+ * Contains: magic (4) + version (4) + totalBytes (8) + writeId (4) + crc32 (4) + reserved (8)
275
276
  */
276
- export const BINARY_CONTENT_HEADER_SIZE = 20;
277
+ export const BINARY_CONTENT_HEADER_SIZE = 32;
277
278
 
278
279
  /**
279
280
  * Size of a single record entry in bytes.