@coreyuan/vector-mind 1.0.42 → 1.0.44

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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
16
16
  import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
17
17
  import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
18
18
  const SERVER_NAME = "vector-mind";
19
- const SERVER_VERSION = "1.0.42";
19
+ const SERVER_VERSION = "1.0.44";
20
20
  const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
21
21
  const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
22
22
  const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
@@ -122,6 +122,48 @@ const INDEX_AUTO_PRUNE_IGNORED = (() => {
122
122
  return true;
123
123
  return ["1", "true", "on", "yes"].includes(raw);
124
124
  })();
125
+ const MAINTENANCE_AUTO_ENABLED = (() => {
126
+ const raw = (process.env.VECTORMIND_MAINTENANCE_AUTO ?? "").trim().toLowerCase();
127
+ if (!raw)
128
+ return true;
129
+ return ["1", "true", "on", "yes"].includes(raw);
130
+ })();
131
+ const MAINTENANCE_INTERVAL_HOURS = (() => {
132
+ const raw = process.env.VECTORMIND_MAINTENANCE_INTERVAL_HOURS?.trim();
133
+ if (!raw)
134
+ return 24;
135
+ const n = Number.parseInt(raw, 10);
136
+ if (!Number.isFinite(n) || n < 1)
137
+ return 24;
138
+ return Math.min(24 * 30, n);
139
+ })();
140
+ const MAINTENANCE_COMPACT_AFTER_DAYS = (() => {
141
+ const raw = process.env.VECTORMIND_COMPACT_AFTER_DAYS?.trim();
142
+ if (!raw)
143
+ return 45;
144
+ const n = Number.parseInt(raw, 10);
145
+ if (!Number.isFinite(n) || n < 1)
146
+ return 45;
147
+ return Math.min(3650, n);
148
+ })();
149
+ const MAINTENANCE_MAX_MEMORY_ITEMS = (() => {
150
+ const raw = process.env.VECTORMIND_MAINTENANCE_MAX_MEMORY_ITEMS?.trim();
151
+ if (!raw)
152
+ return 250;
153
+ const n = Number.parseInt(raw, 10);
154
+ if (!Number.isFinite(n) || n < 1)
155
+ return 250;
156
+ return Math.min(5000, n);
157
+ })();
158
+ const MAINTENANCE_MAX_INDEX_FILES = (() => {
159
+ const raw = process.env.VECTORMIND_MAINTENANCE_MAX_INDEX_FILES?.trim();
160
+ if (!raw)
161
+ return 1500;
162
+ const n = Number.parseInt(raw, 10);
163
+ if (!Number.isFinite(n) || n < 1)
164
+ return 1500;
165
+ return Math.min(50_000, n);
166
+ })();
125
167
  const ROOTS_LIST_TIMEOUT_MS = (() => {
126
168
  const raw = process.env.VECTORMIND_ROOTS_TIMEOUT_MS?.trim();
127
169
  if (!raw)
@@ -140,6 +182,15 @@ const BOOTSTRAP_SEMANTIC_TIMEOUT_MS = (() => {
140
182
  return 2500;
141
183
  return n;
142
184
  })();
185
+ const SEMANTIC_EMBEDDINGS_TIMEOUT_MS = (() => {
186
+ const raw = process.env.VECTORMIND_EMBEDDINGS_TIMEOUT_MS?.trim();
187
+ if (!raw)
188
+ return 1500;
189
+ const n = Number.parseInt(raw, 10);
190
+ if (!Number.isFinite(n) || n <= 0)
191
+ return 1500;
192
+ return n;
193
+ })();
143
194
  let initialized = false;
144
195
  let rootSource = "cwd";
145
196
  let projectRoot = "";
@@ -150,6 +201,7 @@ let watcherReady = false;
150
201
  let initializationPromise = null;
151
202
  let insertRequirementStmt;
152
203
  let getActiveRequirementStmt;
204
+ let listActiveRequirementsStmt;
153
205
  let listRecentRequirementsStmt;
154
206
  let completeAllActiveRequirementsStmt;
155
207
  let completeRequirementByIdStmt;
@@ -170,6 +222,7 @@ let listCurrentDecisionsStmt;
170
222
  let upsertProjectSummaryStmt;
171
223
  let getProjectSummaryStmt;
172
224
  let listRecentNotesStmt;
225
+ let listRecentContextItemsStmt;
173
226
  let getLatestChangeIntentForFileStmt;
174
227
  let deleteFileChunkItemsStmt;
175
228
  let getEmbeddingMetaStmt;
@@ -189,6 +242,8 @@ let insertTokenSavingsStmt;
189
242
  let summarizeTokenSavingsStmt;
190
243
  let summarizeTokenSavingsByToolStmt;
191
244
  let listRecentTokenSavingsStmt;
245
+ let getKvStmt;
246
+ let setKvStmt;
192
247
  let indexFileSymbolsTx = null;
193
248
  let activitySeq = 0;
194
249
  const activityLog = [];
@@ -284,6 +339,8 @@ function summarizeActivityEvent(e) {
284
339
  return `sync_change_intent #${String(d.req_id ?? "")} files=${String(d.files_total ?? "")}`;
285
340
  case "complete_requirement":
286
341
  return `complete_requirement ${String(d.all_active ? "all_active" : d.req_id ?? "")}`;
342
+ case "memory_maintenance":
343
+ return `memory_maintenance trigger=${String(d.trigger ?? "")} compacted=${String(d.compacted ?? "")} stale=${String(d.stale_files ?? "")} chunks_deleted=${String(d.chunks_deleted ?? "")}`;
287
344
  default:
288
345
  return e.type;
289
346
  }
@@ -723,9 +780,9 @@ function pruneFilenameNoiseIndexes() {
723
780
  return { chunks_deleted: 0, symbols_deleted: 0 };
724
781
  try {
725
782
  const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
726
- const baseWhere = NOISE_FILE_BASENAMES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
783
+ const baseWhere = NOISE_FILE_BASENAMES.map(() => "(LOWER(file_path) = ? OR LOWER(file_path) LIKE ?)").join(" OR ");
727
784
  const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
728
- const baseArgs = NOISE_FILE_BASENAMES.map((n) => `%/${n}`);
785
+ const baseArgs = NOISE_FILE_BASENAMES.flatMap((n) => [n, `%/${n}`]);
729
786
  const whereParts = [];
730
787
  const args = [];
731
788
  if (suffixWhere) {
@@ -764,6 +821,446 @@ function pruneFilenameNoiseIndexes() {
764
821
  return { chunks_deleted: 0, symbols_deleted: 0 };
765
822
  }
766
823
  }
824
+ function kvGet(key) {
825
+ try {
826
+ const row = getKvStmt?.get(key);
827
+ return row?.value ?? null;
828
+ }
829
+ catch {
830
+ return null;
831
+ }
832
+ }
833
+ function kvSet(key, value) {
834
+ try {
835
+ setKvStmt?.run(key, value);
836
+ }
837
+ catch (err) {
838
+ console.error("[vectormind] kv set failed:", err);
839
+ }
840
+ }
841
+ function distinctChunkAndSymbolFilePaths(limit) {
842
+ const rows = db
843
+ .prepare(`SELECT file_path
844
+ FROM (
845
+ SELECT file_path, MAX(updated_at) AS updated_at
846
+ FROM memory_items
847
+ WHERE file_path IS NOT NULL
848
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
849
+ GROUP BY file_path
850
+ UNION
851
+ SELECT file_path, CURRENT_TIMESTAMP AS updated_at
852
+ FROM symbols
853
+ WHERE file_path IS NOT NULL
854
+ GROUP BY file_path
855
+ )
856
+ WHERE file_path IS NOT NULL
857
+ ORDER BY updated_at ASC
858
+ LIMIT ?`)
859
+ .all(limit);
860
+ return Array.from(new Set(rows.map((r) => r.file_path).filter(Boolean)));
861
+ }
862
+ function classifyStaleIndexFile(filePath) {
863
+ if (!filePath)
864
+ return "empty_path";
865
+ if (shouldIgnoreDbFilePath(filePath))
866
+ return "ignored_path";
867
+ if (shouldIgnoreContentFile(filePath))
868
+ return "filename_noise";
869
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
870
+ const rel = path.relative(projectRoot, absPath);
871
+ if (rel.startsWith("..") || path.isAbsolute(rel))
872
+ return "outside_project";
873
+ let stat;
874
+ try {
875
+ stat = fs.statSync(absPath);
876
+ }
877
+ catch {
878
+ return "missing_file";
879
+ }
880
+ if (!stat.isFile())
881
+ return "not_file";
882
+ if (!isContentIndexableFile(absPath) && !isSymbolIndexableFile(absPath))
883
+ return "not_indexable";
884
+ return null;
885
+ }
886
+ function pruneStaleFileIndexes(opts) {
887
+ const filePaths = distinctChunkAndSymbolFilePaths(Math.min(50_000, opts.maxIndexFiles * 3));
888
+ const matched = [];
889
+ for (const fp of filePaths) {
890
+ if (matched.length >= opts.maxIndexFiles)
891
+ break;
892
+ const reason = classifyStaleIndexFile(fp);
893
+ if (reason)
894
+ matched.push({ file_path: fp, reason });
895
+ }
896
+ let chunksDeleted = 0;
897
+ let symbolsDeleted = 0;
898
+ const samples = matched.slice(0, 20).map((m) => `${m.file_path} (${m.reason})`);
899
+ if (!opts.dryRun && matched.length) {
900
+ const tx = db.transaction(() => {
901
+ for (const m of matched) {
902
+ chunksDeleted += deleteFileChunkItemsStmt.run(m.file_path).changes;
903
+ symbolsDeleted += deleteSymbolsForFileStmt.run(m.file_path).changes;
904
+ }
905
+ });
906
+ try {
907
+ tx();
908
+ }
909
+ catch (err) {
910
+ console.error("[vectormind] prune stale indexes failed:", err);
911
+ }
912
+ }
913
+ else if (opts.dryRun && matched.length) {
914
+ const countChunksStmt = db.prepare(`SELECT COUNT(1) AS c
915
+ FROM memory_items
916
+ WHERE file_path = ?
917
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
918
+ const countSymbolsStmt = db.prepare(`SELECT COUNT(1) AS c FROM symbols WHERE file_path = ?`);
919
+ for (const m of matched) {
920
+ chunksDeleted += Number(countChunksStmt.get(m.file_path)?.c ?? 0);
921
+ symbolsDeleted += Number(countSymbolsStmt.get(m.file_path)?.c ?? 0);
922
+ }
923
+ }
924
+ if (!opts.dryRun && (chunksDeleted || symbolsDeleted)) {
925
+ logActivity("index_prune", {
926
+ reason: "stale_files",
927
+ files_matched: matched.length,
928
+ chunks_deleted: chunksDeleted,
929
+ symbols_deleted: symbolsDeleted,
930
+ samples,
931
+ });
932
+ }
933
+ return {
934
+ files_checked: filePaths.length,
935
+ files_matched: matched.length,
936
+ chunks_deleted: chunksDeleted,
937
+ symbols_deleted: symbolsDeleted,
938
+ samples,
939
+ };
940
+ }
941
+ function countIgnoredIndexDeletes() {
942
+ if (!IGNORED_LIKE_PATTERNS.length)
943
+ return { chunks_deleted: 0, symbols_deleted: 0 };
944
+ const where = IGNORED_LIKE_PATTERNS
945
+ .map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
946
+ .join(" OR ");
947
+ const chunksDeleted = Number(db
948
+ .prepare(`SELECT COUNT(1) AS c
949
+ FROM memory_items
950
+ WHERE file_path IS NOT NULL
951
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
952
+ AND (${where})`)
953
+ .get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
954
+ const symbolsDeleted = Number(db
955
+ .prepare(`SELECT COUNT(1) AS c
956
+ FROM symbols
957
+ WHERE file_path IS NOT NULL
958
+ AND (${where})`)
959
+ .get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
960
+ return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
961
+ }
962
+ function countFilenameNoiseIndexDeletes() {
963
+ const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
964
+ const baseWhere = NOISE_FILE_BASENAMES.map(() => "(LOWER(file_path) = ? OR LOWER(file_path) LIKE ?)").join(" OR ");
965
+ const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
966
+ const baseArgs = NOISE_FILE_BASENAMES.flatMap((n) => [n, `%/${n}`]);
967
+ const whereParts = [];
968
+ const args = [];
969
+ if (suffixWhere) {
970
+ whereParts.push(`(${suffixWhere})`);
971
+ args.push(...suffixArgs);
972
+ }
973
+ if (baseWhere) {
974
+ whereParts.push(`(${baseWhere})`);
975
+ args.push(...baseArgs);
976
+ }
977
+ if (!whereParts.length)
978
+ return { chunks_deleted: 0, symbols_deleted: 0 };
979
+ const where = whereParts.join(" OR ");
980
+ const chunksDeleted = Number(db
981
+ .prepare(`SELECT COUNT(1) AS c
982
+ FROM memory_items
983
+ WHERE file_path IS NOT NULL
984
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
985
+ AND (${where})`)
986
+ .get(...args)?.c ?? 0);
987
+ const symbolsDeleted = Number(db
988
+ .prepare(`SELECT COUNT(1) AS c
989
+ FROM symbols
990
+ WHERE file_path IS NOT NULL
991
+ AND (${where})`)
992
+ .get(...args)?.c ?? 0);
993
+ return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
994
+ }
995
+ function hiddenEmbeddingIds(limit = 10_000) {
996
+ const rows = db
997
+ .prepare(`SELECT e.memory_id AS memory_id, m.metadata_json AS metadata_json
998
+ FROM embeddings e
999
+ JOIN memory_items m ON m.id = e.memory_id
1000
+ WHERE m.metadata_json LIKE '%compacted%'
1001
+ OR m.metadata_json LIKE '%superseded%'
1002
+ LIMIT ?`)
1003
+ .all(limit);
1004
+ return rows
1005
+ .filter((r) => isHiddenFromDefaultRecall({ metadata_json: r.metadata_json }))
1006
+ .map((r) => r.memory_id);
1007
+ }
1008
+ function pruneHiddenEmbeddings(dryRun) {
1009
+ const ids = hiddenEmbeddingIds();
1010
+ if (!ids.length)
1011
+ return { embeddings_deleted: 0 };
1012
+ if (!dryRun) {
1013
+ const deleteStmt = db.prepare(`DELETE FROM embeddings WHERE memory_id = ?`);
1014
+ const tx = db.transaction(() => {
1015
+ for (const id of ids)
1016
+ deleteStmt.run(id);
1017
+ });
1018
+ try {
1019
+ tx();
1020
+ }
1021
+ catch (err) {
1022
+ console.error("[vectormind] prune hidden embeddings failed:", err);
1023
+ }
1024
+ }
1025
+ return { embeddings_deleted: ids.length };
1026
+ }
1027
+ function selectCompactionCandidates(opts) {
1028
+ const kinds = opts.compactNotes
1029
+ ? ["requirement", "change_intent", "note"]
1030
+ : ["requirement", "change_intent"];
1031
+ const placeholders = kinds.map(() => "?").join(", ");
1032
+ const rows = db
1033
+ .prepare(`SELECT
1034
+ m.id, m.kind, m.title, m.content, m.file_path, m.start_line, m.end_line,
1035
+ m.req_id, m.metadata_json, m.content_hash, m.created_at, m.updated_at,
1036
+ r.status AS req_status
1037
+ FROM memory_items m
1038
+ LEFT JOIN requirements r ON r.id = m.req_id
1039
+ WHERE m.kind IN (${placeholders})
1040
+ AND m.updated_at < datetime('now', ?)
1041
+ ORDER BY m.updated_at ASC, m.id ASC
1042
+ LIMIT ?`)
1043
+ .all(...kinds, `-${opts.compactAfterDays} days`, Math.min(20_000, opts.maxMemoryItems * 5));
1044
+ return rows
1045
+ .filter((row) => !isHiddenFromDefaultRecall(row))
1046
+ .filter((row) => metadataStatus(row) !== "current" && metadataStatus(row) !== "active")
1047
+ .filter((row) => row.req_status !== "active")
1048
+ .filter((row) => row.kind !== "note" || opts.compactNotes)
1049
+ .slice(0, opts.maxMemoryItems);
1050
+ }
1051
+ function compactionLine(row) {
1052
+ const date = oneLine(row.updated_at || row.created_at, 19);
1053
+ const title = row.title ? ` ${oneLine(row.title, 80)}` : "";
1054
+ const file = row.file_path ? ` file=${row.file_path}${row.start_line != null ? `:${row.start_line}` : ""}` : "";
1055
+ const req = row.req_id != null ? ` req#${row.req_id}` : "";
1056
+ return `- ${date} #${row.id} ${row.kind}${req}${file}${title}: ${oneLine(row.content, 220)}`;
1057
+ }
1058
+ function compactOldMemoryItems(opts) {
1059
+ const candidates = selectCompactionCandidates(opts);
1060
+ const cutoff = new Date(Date.now() - opts.compactAfterDays * 86_400_000).toISOString();
1061
+ const samples = candidates.slice(0, 20).map((row) => ({
1062
+ id: row.id,
1063
+ kind: row.kind,
1064
+ title: row.title,
1065
+ file_path: row.file_path,
1066
+ updated_at: row.updated_at,
1067
+ }));
1068
+ if (opts.dryRun || !candidates.length) {
1069
+ return {
1070
+ cutoff,
1071
+ candidates: candidates.length,
1072
+ compacted: 0,
1073
+ summary_memory_id: null,
1074
+ archived: 0,
1075
+ samples,
1076
+ };
1077
+ }
1078
+ const now = new Date().toISOString();
1079
+ const lines = [
1080
+ `Auto-compacted ${candidates.length} old VectorMind memory items.`,
1081
+ `Cutoff: items updated before ${cutoff} (${opts.compactAfterDays} days).`,
1082
+ "",
1083
+ "This compact summary keeps old history searchable while detailed stale items are hidden from default recall.",
1084
+ "Durable decisions, conventions, and project summaries are never compacted by this automatic pass.",
1085
+ "",
1086
+ ...candidates.map(compactionLine),
1087
+ ];
1088
+ const content = lines.join("\n");
1089
+ const title = `Memory compaction ${now.slice(0, 10)}`;
1090
+ const metadata = {
1091
+ source: "maintenance",
1092
+ status: "current",
1093
+ compacted_item_ids: candidates.map((c) => c.id),
1094
+ compact_after_days: opts.compactAfterDays,
1095
+ compact_notes: opts.compactNotes,
1096
+ generated_at: now,
1097
+ };
1098
+ let summaryMemoryId = 0;
1099
+ let archived = 0;
1100
+ const archiveStmt = db.prepare(`INSERT OR IGNORE INTO memory_item_archive
1101
+ (memory_id, original_kind, original_title, original_content, original_file_path,
1102
+ original_start_line, original_end_line, original_req_id, original_metadata_json,
1103
+ original_content_hash, original_created_at, original_updated_at, archive_reason, compacted_into_id)
1104
+ VALUES
1105
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1106
+ const updateMemoryStmt = db.prepare(`UPDATE memory_items
1107
+ SET content = ?, metadata_json = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP
1108
+ WHERE id = ?`);
1109
+ const deleteEmbeddingStmt = db.prepare(`DELETE FROM embeddings WHERE memory_id = ?`);
1110
+ const tx = db.transaction(() => {
1111
+ const info = insertMemoryItemStmt.run("memory_compaction", title, content, null, null, null, null, safeJson(metadata), sha256Hex(content));
1112
+ summaryMemoryId = Number(info.lastInsertRowid);
1113
+ for (const row of candidates) {
1114
+ const archiveInfo = archiveStmt.run(row.id, row.kind, row.title, row.content, row.file_path, row.start_line, row.end_line, row.req_id, row.metadata_json, row.content_hash, row.created_at, row.updated_at, "auto_compaction", summaryMemoryId);
1115
+ if (archiveInfo.changes > 0)
1116
+ archived += 1;
1117
+ const patchedMeta = {
1118
+ ...parseMetadataJson(row.metadata_json),
1119
+ status: "compacted",
1120
+ compacted: true,
1121
+ compacted_at: now,
1122
+ compacted_into_memory_id: summaryMemoryId,
1123
+ };
1124
+ const stub = [
1125
+ `[compacted into memory item #${summaryMemoryId}]`,
1126
+ `Original ${row.kind} #${row.id} was older than ${opts.compactAfterDays} days and is excluded from default recall.`,
1127
+ `Summary: ${oneLine(row.title || row.content, 260)}`,
1128
+ ].join("\n");
1129
+ updateMemoryStmt.run(stub, safeJson(patchedMeta), sha256Hex(stub), row.id);
1130
+ deleteEmbeddingStmt.run(row.id);
1131
+ }
1132
+ });
1133
+ try {
1134
+ tx();
1135
+ if (summaryMemoryId)
1136
+ enqueueEmbedding(summaryMemoryId);
1137
+ }
1138
+ catch (err) {
1139
+ console.error("[vectormind] compact old memory failed:", err);
1140
+ summaryMemoryId = 0;
1141
+ }
1142
+ if (summaryMemoryId) {
1143
+ logActivity("memory_maintenance", {
1144
+ reason: "compact_old_memories",
1145
+ compacted: candidates.length,
1146
+ summary_memory_id: summaryMemoryId,
1147
+ archived,
1148
+ });
1149
+ }
1150
+ return {
1151
+ cutoff,
1152
+ candidates: candidates.length,
1153
+ compacted: summaryMemoryId ? candidates.length : 0,
1154
+ summary_memory_id: summaryMemoryId || null,
1155
+ archived,
1156
+ samples,
1157
+ };
1158
+ }
1159
+ function runMemoryMaintenance(args, trigger = "manual") {
1160
+ const compactedMemory = args.compact_old_memories
1161
+ ? compactOldMemoryItems({
1162
+ dryRun: args.dry_run,
1163
+ compactAfterDays: args.compact_after_days,
1164
+ maxMemoryItems: args.max_memory_items,
1165
+ compactNotes: args.compact_notes,
1166
+ })
1167
+ : {
1168
+ cutoff: new Date(Date.now() - args.compact_after_days * 86_400_000).toISOString(),
1169
+ candidates: 0,
1170
+ compacted: 0,
1171
+ summary_memory_id: null,
1172
+ archived: 0,
1173
+ samples: [],
1174
+ };
1175
+ const ignoredPaths = args.prune_ignored_paths
1176
+ ? args.dry_run
1177
+ ? countIgnoredIndexDeletes()
1178
+ : pruneIgnoredIndexesByPathPatterns()
1179
+ : { chunks_deleted: 0, symbols_deleted: 0 };
1180
+ const filenameNoise = args.prune_filename_noise
1181
+ ? args.dry_run
1182
+ ? countFilenameNoiseIndexDeletes()
1183
+ : pruneFilenameNoiseIndexes()
1184
+ : { chunks_deleted: 0, symbols_deleted: 0 };
1185
+ const staleFiles = args.prune_stale_indexes
1186
+ ? pruneStaleFileIndexes({ dryRun: args.dry_run, maxIndexFiles: args.max_index_files })
1187
+ : { files_checked: 0, files_matched: 0, chunks_deleted: 0, symbols_deleted: 0, samples: [] };
1188
+ const hiddenEmbeddings = args.prune_hidden_embeddings
1189
+ ? pruneHiddenEmbeddings(args.dry_run)
1190
+ : { embeddings_deleted: 0 };
1191
+ let vacuumed = false;
1192
+ if (!args.dry_run && args.vacuum) {
1193
+ try {
1194
+ db.exec("VACUUM");
1195
+ vacuumed = true;
1196
+ }
1197
+ catch (err) {
1198
+ console.error("[vectormind] maintenance vacuum failed:", err);
1199
+ }
1200
+ }
1201
+ const result = {
1202
+ ok: true,
1203
+ dry_run: args.dry_run,
1204
+ trigger,
1205
+ generated_at: new Date().toISOString(),
1206
+ project_root: projectRoot,
1207
+ db_path: dbPath,
1208
+ config: {
1209
+ compact_after_days: args.compact_after_days,
1210
+ max_memory_items: args.max_memory_items,
1211
+ max_index_files: args.max_index_files,
1212
+ compact_notes: args.compact_notes,
1213
+ },
1214
+ compacted_memory: compactedMemory,
1215
+ pruned: {
1216
+ ignored_paths: ignoredPaths,
1217
+ filename_noise: filenameNoise,
1218
+ stale_files: staleFiles,
1219
+ hidden_embeddings: hiddenEmbeddings,
1220
+ },
1221
+ vacuumed,
1222
+ };
1223
+ logActivity("memory_maintenance", {
1224
+ trigger,
1225
+ dry_run: args.dry_run,
1226
+ compacted: result.compacted_memory.compacted,
1227
+ stale_files: result.pruned.stale_files.files_matched,
1228
+ chunks_deleted: result.pruned.ignored_paths.chunks_deleted +
1229
+ result.pruned.filename_noise.chunks_deleted +
1230
+ result.pruned.stale_files.chunks_deleted,
1231
+ });
1232
+ return result;
1233
+ }
1234
+ function runAutoMaintenanceIfDue() {
1235
+ if (!MAINTENANCE_AUTO_ENABLED || !db)
1236
+ return;
1237
+ const lastRaw = kvGet("maintenance.last_auto_at");
1238
+ const last = lastRaw ? Date.parse(lastRaw) : 0;
1239
+ const dueMs = MAINTENANCE_INTERVAL_HOURS * 3_600_000;
1240
+ if (Number.isFinite(last) && last > 0 && Date.now() - last < dueMs)
1241
+ return;
1242
+ try {
1243
+ runMemoryMaintenance({
1244
+ project_root: projectRoot,
1245
+ dry_run: false,
1246
+ format: "compact",
1247
+ compact_old_memories: true,
1248
+ compact_notes: false,
1249
+ prune_stale_indexes: true,
1250
+ prune_ignored_paths: true,
1251
+ prune_filename_noise: true,
1252
+ prune_hidden_embeddings: true,
1253
+ compact_after_days: MAINTENANCE_COMPACT_AFTER_DAYS,
1254
+ max_memory_items: MAINTENANCE_MAX_MEMORY_ITEMS,
1255
+ max_index_files: MAINTENANCE_MAX_INDEX_FILES,
1256
+ vacuum: false,
1257
+ }, "auto");
1258
+ kvSet("maintenance.last_auto_at", new Date().toISOString());
1259
+ }
1260
+ catch (err) {
1261
+ console.error("[vectormind] auto maintenance failed:", err);
1262
+ }
1263
+ }
767
1264
  function shouldIgnorePath(inputPath) {
768
1265
  const normalizedAbs = path.resolve(inputPath);
769
1266
  const rel = path.relative(projectRoot, normalizedAbs);
@@ -1352,6 +1849,19 @@ const SupersedeMemoryArgsSchema = ProjectRootArgSchema.merge(z.object({
1352
1849
  replacement_memory_id: z.number().int().positive().optional(),
1353
1850
  reason: z.string().min(1),
1354
1851
  }));
1852
+ const MaintainMemoryArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1853
+ dry_run: z.boolean().optional().default(true),
1854
+ compact_old_memories: z.boolean().optional().default(true),
1855
+ compact_notes: z.boolean().optional().default(false),
1856
+ prune_stale_indexes: z.boolean().optional().default(true),
1857
+ prune_ignored_paths: z.boolean().optional().default(true),
1858
+ prune_filename_noise: z.boolean().optional().default(true),
1859
+ prune_hidden_embeddings: z.boolean().optional().default(true),
1860
+ compact_after_days: z.number().int().min(1).max(3650).optional().default(MAINTENANCE_COMPACT_AFTER_DAYS),
1861
+ max_memory_items: z.number().int().min(1).max(5000).optional().default(MAINTENANCE_MAX_MEMORY_ITEMS),
1862
+ max_index_files: z.number().int().min(1).max(50_000).optional().default(MAINTENANCE_MAX_INDEX_FILES),
1863
+ vacuum: z.boolean().optional().default(false),
1864
+ }));
1355
1865
  const DEFAULT_PENDING_LIMIT = 10;
1356
1866
  const MAX_PENDING_LIMIT = 2000;
1357
1867
  const PendingPagingSchema = z.object({
@@ -1371,13 +1881,22 @@ const DEFAULT_RECENT_CHANGES_PER_REQ = 3;
1371
1881
  const DEFAULT_RECENT_NOTES = 3;
1372
1882
  const DEFAULT_CONVENTIONS_LIMIT = 0;
1373
1883
  const DEFAULT_DECISIONS_LIMIT = 5;
1884
+ const DEFAULT_CURRENT_CONTEXT_LIMIT = 8;
1374
1885
  const MAX_DECISIONS_LIMIT = 50;
1886
+ const MAX_CURRENT_CONTEXT_LIMIT = 50;
1375
1887
  const BrainDumpLimitsSchema = z.object({
1376
1888
  requirements_limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECENT_REQUIREMENTS),
1377
1889
  changes_limit: z.number().int().min(1).max(100).optional().default(DEFAULT_RECENT_CHANGES_PER_REQ),
1378
1890
  notes_limit: z.number().int().min(0).max(50).optional().default(DEFAULT_RECENT_NOTES),
1379
1891
  conventions_limit: z.number().int().min(0).max(200).optional().default(DEFAULT_CONVENTIONS_LIMIT),
1380
1892
  decisions_limit: z.number().int().min(0).max(MAX_DECISIONS_LIMIT).optional().default(DEFAULT_DECISIONS_LIMIT),
1893
+ current_context_limit: z
1894
+ .number()
1895
+ .int()
1896
+ .min(0)
1897
+ .max(MAX_CURRENT_CONTEXT_LIMIT)
1898
+ .optional()
1899
+ .default(DEFAULT_CURRENT_CONTEXT_LIMIT),
1381
1900
  });
1382
1901
  const GetPendingChangesArgsSchema = ProjectRootArgSchema.merge(z.object({
1383
1902
  offset: z.number().int().min(0).optional().default(0),
@@ -1605,6 +2124,33 @@ function compactQueryCodebaseText(data) {
1605
2124
  lines.push("- no matches");
1606
2125
  return lines.join("\n");
1607
2126
  }
2127
+ function compactMaintenanceText(data) {
2128
+ const prunedChunks = data.pruned.ignored_paths.chunks_deleted +
2129
+ data.pruned.filename_noise.chunks_deleted +
2130
+ data.pruned.stale_files.chunks_deleted;
2131
+ const prunedSymbols = data.pruned.ignored_paths.symbols_deleted +
2132
+ data.pruned.filename_noise.symbols_deleted +
2133
+ data.pruned.stale_files.symbols_deleted;
2134
+ const lines = [
2135
+ `maintain_memory ok dry_run=${data.dry_run} trigger=${data.trigger} compacted=${data.compacted_memory.compacted}/${data.compacted_memory.candidates} archived=${data.compacted_memory.archived} pruned_chunks=${prunedChunks} pruned_symbols=${prunedSymbols} hidden_embeddings=${data.pruned.hidden_embeddings.embeddings_deleted}`,
2136
+ ];
2137
+ if (data.compacted_memory.summary_memory_id) {
2138
+ lines.push(`summary memory_compaction #${data.compacted_memory.summary_memory_id}`);
2139
+ }
2140
+ if (data.compacted_memory.samples.length) {
2141
+ lines.push("memory candidates:");
2142
+ for (const s of data.compacted_memory.samples.slice(0, 8)) {
2143
+ lines.push(`- #${s.id} ${s.kind} ${s.file_path ?? ""} ${oneLine(s.title ?? "", 80)} ${s.updated_at}`);
2144
+ }
2145
+ }
2146
+ if (data.pruned.stale_files.samples.length) {
2147
+ lines.push("stale index samples:");
2148
+ for (const s of data.pruned.stale_files.samples.slice(0, 8))
2149
+ lines.push(`- ${s}`);
2150
+ }
2151
+ lines.push("hint: dry_run=false applies changes; vacuum=true reclaims sqlite file space after pruning");
2152
+ return lines.join("\n");
2153
+ }
1608
2154
  function compactBootstrapText(data) {
1609
2155
  const lines = [];
1610
2156
  lines.push(`ok ctx ${data.root_source} watcher=${data.watcher_enabled ? (data.watcher_ready ? "ready" : "starting") : "off"} root=${data.project_root}`);
@@ -1615,6 +2161,11 @@ function compactBootstrapText(data) {
1615
2161
  for (const d of data.decisions.slice(0, 5))
1616
2162
  lines.push(`- ${compactMemoryLabel(d, 160)}`);
1617
2163
  }
2164
+ if (data.current_context.length) {
2165
+ lines.push("current context:");
2166
+ for (const c of data.current_context.slice(0, 8))
2167
+ lines.push(`- ${compactMemoryLabel(c, 160)}`);
2168
+ }
1618
2169
  if (data.pending_total) {
1619
2170
  lines.push(`pending ${data.pending_changes.length}/${data.pending_total}${data.pending_truncated ? " truncated" : ""}: ${data.pending_changes
1620
2171
  .slice(0, 8)
@@ -1719,8 +2270,8 @@ function runRtkProbe(spec) {
1719
2270
  ? `Prefer prefixing shell commands with ${spec.displayCommand} for compact outputs. This is VectorMind's bundled RTK shim; first run auto-installs/caches rtk-ai/rtk if needed.`
1720
2271
  : "Prefer prefixing shell commands with rtk for compact outputs, e.g. rtk git status / rtk npm run build / rtk rg pattern ."
1721
2272
  : spec.source === "package_shim"
1722
- ? "VectorMind's bundled RTK shim exists, but `gain` failed. Check network/cache or set VECTORMIND_RTK_REAL to an existing rtk-ai/rtk binary."
1723
- : "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only after confirming it is safe.",
2273
+ ? "VectorMind's bundled RTK shim exists, but `gain` failed. Check npm/cache or set VECTORMIND_RTK_REAL to an existing rtk-ai/rtk binary."
2274
+ : "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only when you intentionally want to replace it.",
1724
2275
  };
1725
2276
  }
1726
2277
  return null;
@@ -1755,7 +2306,7 @@ function detectRtk() {
1755
2306
  path: shimPath ?? undefined,
1756
2307
  source: shimPath ? "package_shim" : undefined,
1757
2308
  note: shimPath
1758
- ? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check network/cache or set VECTORMIND_RTK_REAL."
2309
+ ? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check npm/cache or set VECTORMIND_RTK_REAL."
1759
2310
  : "rtk was not found on PATH and the package RTK shim is unavailable. VectorMind compact MCP output still works; install rtk to compact shell command output too.",
1760
2311
  };
1761
2312
  }
@@ -2129,6 +2680,80 @@ function dotProduct(a, b) {
2129
2680
  s += a[i] * b[i];
2130
2681
  return s;
2131
2682
  }
2683
+ const SEMANTIC_TOKEN_STOPWORDS = new Set([
2684
+ "a",
2685
+ "an",
2686
+ "and",
2687
+ "are",
2688
+ "as",
2689
+ "at",
2690
+ "be",
2691
+ "by",
2692
+ "for",
2693
+ "from",
2694
+ "has",
2695
+ "have",
2696
+ "if",
2697
+ "in",
2698
+ "into",
2699
+ "is",
2700
+ "it",
2701
+ "its",
2702
+ "of",
2703
+ "on",
2704
+ "or",
2705
+ "that",
2706
+ "the",
2707
+ "this",
2708
+ "to",
2709
+ "with",
2710
+ ]);
2711
+ const BOOTSTRAP_DEFAULT_CONTEXT_KINDS = [
2712
+ "decision",
2713
+ "convention",
2714
+ "project_summary",
2715
+ "memory_compaction",
2716
+ "note",
2717
+ "requirement",
2718
+ "change_intent",
2719
+ ];
2720
+ const TOKEN_SEARCH_DEFAULT_KINDS = [
2721
+ "decision",
2722
+ "convention",
2723
+ "project_summary",
2724
+ "memory_compaction",
2725
+ "note",
2726
+ "requirement",
2727
+ "change_intent",
2728
+ "code_chunk",
2729
+ "doc_chunk",
2730
+ ];
2731
+ const DECISION_CANDIDATE_KEYWORDS = [
2732
+ "用户确认",
2733
+ "用户要求",
2734
+ "明确",
2735
+ "架构决策",
2736
+ "最终",
2737
+ "默认",
2738
+ "只保留",
2739
+ "统一",
2740
+ "不需要",
2741
+ "无需",
2742
+ "不再",
2743
+ "改成",
2744
+ "改为",
2745
+ "直接通过",
2746
+ "不用审核",
2747
+ "decision",
2748
+ "decided",
2749
+ "confirmed",
2750
+ "must",
2751
+ "default",
2752
+ "only",
2753
+ "single",
2754
+ "no longer",
2755
+ "instead",
2756
+ ];
2132
2757
  function parseMetadataJson(metadata) {
2133
2758
  if (!metadata)
2134
2759
  return {};
@@ -2150,6 +2775,13 @@ function isSupersededMemory(row) {
2150
2775
  const meta = parseMetadataJson(row.metadata_json);
2151
2776
  return meta.superseded === true || meta.status === "superseded";
2152
2777
  }
2778
+ function isCompactedMemory(row) {
2779
+ const meta = parseMetadataJson(row.metadata_json);
2780
+ return meta.compacted === true || meta.status === "compacted";
2781
+ }
2782
+ function isHiddenFromDefaultRecall(row) {
2783
+ return isSupersededMemory(row) || isCompactedMemory(row);
2784
+ }
2153
2785
  function semanticRecencyWeight(updatedAt) {
2154
2786
  if (!updatedAt)
2155
2787
  return 0;
@@ -2168,7 +2800,7 @@ function semanticRecencyWeight(updatedAt) {
2168
2800
  function semanticKindWeight(kind) {
2169
2801
  switch (kind) {
2170
2802
  case "decision":
2171
- return 3.5;
2803
+ return 16;
2172
2804
  case "convention":
2173
2805
  return 2.6;
2174
2806
  case "project_summary":
@@ -2179,16 +2811,20 @@ function semanticKindWeight(kind) {
2179
2811
  return 0.4;
2180
2812
  case "change_intent":
2181
2813
  return 0.2;
2814
+ case "memory_compaction":
2815
+ return 0.7;
2182
2816
  default:
2183
2817
  return 0;
2184
2818
  }
2185
2819
  }
2186
2820
  function adjustSemanticScore(row, rawScore) {
2187
- if (isSupersededMemory(row))
2821
+ if (isHiddenFromDefaultRecall(row))
2188
2822
  return rawScore - 1000;
2189
2823
  let score = rawScore + semanticKindWeight(row.kind) + semanticRecencyWeight(row.updated_at);
2190
2824
  const status = metadataStatus(row);
2191
- if (status === "active" || status === "current")
2825
+ if (status === "current")
2826
+ score += row.kind === "decision" ? 24 : 1.2;
2827
+ if (status === "active")
2192
2828
  score += 1.2;
2193
2829
  if (row.kind === "change_intent" && row.file_path && shouldIgnoreDbFilePath(row.file_path)) {
2194
2830
  // Human-synced intent for generated/build/runtime files is often the only durable
@@ -2197,11 +2833,110 @@ function adjustSemanticScore(row, rawScore) {
2197
2833
  }
2198
2834
  return score;
2199
2835
  }
2836
+ function normalizeSearchText(input) {
2837
+ return (input ?? "").normalize("NFKC").toLowerCase();
2838
+ }
2839
+ function extractSearchTokens(raw) {
2840
+ const text = normalizeSearchText(raw);
2841
+ const tokens = new Set();
2842
+ for (const token of text.match(/[a-z0-9_./:@#-]{2,}/g) ?? []) {
2843
+ if (!SEMANTIC_TOKEN_STOPWORDS.has(token))
2844
+ tokens.add(token);
2845
+ for (const part of token.split(/[^a-z0-9]+/).filter((p) => p.length >= 2)) {
2846
+ if (!SEMANTIC_TOKEN_STOPWORDS.has(part))
2847
+ tokens.add(part);
2848
+ }
2849
+ }
2850
+ for (const seq of text.match(/\p{Script=Han}+/gu) ?? []) {
2851
+ if (seq.length >= 2 && seq.length <= 18)
2852
+ tokens.add(seq);
2853
+ for (const n of [2, 3, 4]) {
2854
+ if (seq.length < n)
2855
+ continue;
2856
+ for (let i = 0; i <= seq.length - n; i++) {
2857
+ tokens.add(seq.slice(i, i + n));
2858
+ }
2859
+ }
2860
+ }
2861
+ return Array.from(tokens)
2862
+ .filter((token) => token.length >= 2 && !SEMANTIC_TOKEN_STOPWORDS.has(token))
2863
+ .sort((a, b) => b.length - a.length)
2864
+ .slice(0, 48);
2865
+ }
2866
+ function countNeedleOccurrences(haystack, needle) {
2867
+ if (!haystack || !needle)
2868
+ return 0;
2869
+ let count = 0;
2870
+ let idx = 0;
2871
+ while ((idx = haystack.indexOf(needle, idx)) >= 0) {
2872
+ count++;
2873
+ idx += Math.max(1, needle.length);
2874
+ if (count >= 8)
2875
+ break;
2876
+ }
2877
+ return count;
2878
+ }
2879
+ function tokenLexicalScore(row, query, tokens) {
2880
+ if (!tokens.length)
2881
+ return 0;
2882
+ const title = normalizeSearchText(row.title);
2883
+ const content = normalizeSearchText(row.content);
2884
+ const filePath = normalizeSearchText(row.file_path);
2885
+ const metadata = normalizeSearchText(row.metadata_json);
2886
+ const exact = normalizeSearchText(query).trim();
2887
+ let score = 0;
2888
+ if (exact.length >= 4) {
2889
+ if (title.includes(exact))
2890
+ score += 8;
2891
+ if (content.includes(exact))
2892
+ score += 6;
2893
+ if (filePath.includes(exact))
2894
+ score += 4;
2895
+ }
2896
+ let matched = 0;
2897
+ for (const token of tokens) {
2898
+ let tokenScore = 0;
2899
+ if (title.includes(token))
2900
+ tokenScore += 3.2;
2901
+ if (filePath.includes(token))
2902
+ tokenScore += 2.4;
2903
+ const contentHits = countNeedleOccurrences(content, token);
2904
+ if (contentHits)
2905
+ tokenScore += Math.min(3.2, 0.75 + contentHits * 0.45);
2906
+ if (metadata.includes(token))
2907
+ tokenScore += 0.8;
2908
+ if (tokenScore > 0) {
2909
+ matched++;
2910
+ score += tokenScore * Math.min(2.4, Math.max(1, token.length / 4));
2911
+ }
2912
+ }
2913
+ if (matched >= Math.min(3, tokens.length))
2914
+ score += 2;
2915
+ score += matched / Math.max(1, tokens.length);
2916
+ return score;
2917
+ }
2918
+ function looksLikeDecisionContent(content) {
2919
+ const text = normalizeSearchText(content);
2920
+ return DECISION_CANDIDATE_KEYWORDS.some((kw) => text.includes(normalizeSearchText(kw)));
2921
+ }
2922
+ function mergeSemanticMatches(sets, opts) {
2923
+ const best = new Map();
2924
+ for (const matches of sets) {
2925
+ for (const match of matches) {
2926
+ const prev = best.get(match.item.id);
2927
+ if (!prev || match.score > prev.score)
2928
+ best.set(match.item.id, match);
2929
+ }
2930
+ }
2931
+ return Array.from(best.values())
2932
+ .sort((a, b) => b.score - a.score || b.item.id - a.item.id)
2933
+ .slice(0, opts.topK);
2934
+ }
2200
2935
  function filterAndRankSemanticRows(rows, scoreOf, opts) {
2201
2936
  return rows
2202
2937
  .map((r) => ({ row: r, score: adjustSemanticScore(r, scoreOf(r)) }))
2203
2938
  .filter(({ row }) => {
2204
- if (isSupersededMemory(row))
2939
+ if (isHiddenFromDefaultRecall(row))
2205
2940
  return false;
2206
2941
  if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
2207
2942
  return false;
@@ -2290,10 +3025,43 @@ function getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars) {
2290
3025
  return [];
2291
3026
  const rows = listCurrentDecisionsStmt.all(Math.min(MAX_DECISIONS_LIMIT * 4, Math.max(decisionsLimit, decisionsLimit * 4)));
2292
3027
  return rows
2293
- .filter((d) => !isSupersededMemory(d))
3028
+ .filter((d) => !isHiddenFromDefaultRecall(d))
2294
3029
  .slice(0, decisionsLimit)
2295
3030
  .map((d) => toMemoryItemPreview(d, false, previewChars, contentMaxChars));
2296
3031
  }
3032
+ function getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars) {
3033
+ if (currentContextLimit <= 0)
3034
+ return [];
3035
+ const picked = new Map();
3036
+ const addRow = (row) => {
3037
+ if (!row)
3038
+ return;
3039
+ if (isHiddenFromDefaultRecall(row))
3040
+ return;
3041
+ if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
3042
+ return;
3043
+ if (!picked.has(row.id)) {
3044
+ picked.set(row.id, toMemoryItemPreview(row, false, previewChars, contentMaxChars));
3045
+ }
3046
+ };
3047
+ const activeReqs = listActiveRequirementsStmt.all(Math.max(currentContextLimit, 10));
3048
+ for (const req of activeReqs) {
3049
+ const memId = getRequirementMemoryItemIdStmt.get(req.id)?.id;
3050
+ if (memId != null)
3051
+ addRow(getMemoryItemByIdStmt.get(memId));
3052
+ }
3053
+ const recentRows = listRecentContextItemsStmt.all(Math.max(currentContextLimit * 8, 40));
3054
+ for (const row of recentRows) {
3055
+ if (picked.size >= currentContextLimit)
3056
+ break;
3057
+ if (row.kind === "requirement" || row.kind === "change_intent") {
3058
+ if (!looksLikeDecisionContent(`${row.title ?? ""}\n${row.content}`))
3059
+ continue;
3060
+ }
3061
+ addRow(row);
3062
+ }
3063
+ return Array.from(picked.values()).slice(0, currentContextLimit);
3064
+ }
2297
3065
  function toRequirementPreview(req, includeContent, previewChars, contentMaxChars) {
2298
3066
  const context = req.context_data ?? null;
2299
3067
  const contextPreview = context ? makePreviewText(context, previewChars) : null;
@@ -2431,7 +3199,7 @@ async function semanticSearchInternal(opts) {
2431
3199
  .filter(Boolean);
2432
3200
  const filtered = matches
2433
3201
  .filter((m) => {
2434
- if (isSupersededMemory({ metadata_json: m.item.metadata_json }))
3202
+ if (isHiddenFromDefaultRecall({ metadata_json: m.item.metadata_json }))
2435
3203
  return false;
2436
3204
  if (shouldIgnoreDbFilePath(m.item.file_path) && m.item.kind !== "change_intent")
2437
3205
  return false;
@@ -2591,24 +3359,212 @@ function likeSearchInternal(opts) {
2591
3359
  const matches = filterAndRankSemanticRows(rows, (r) => Number(r.score), opts);
2592
3360
  return { query: q, top_k: opts.topK, mode: "like", matches };
2593
3361
  }
2594
- async function semanticSearchHybridInternal(opts) {
2595
- if (embeddingsEnabled) {
2596
- try {
2597
- return await semanticSearchInternal(opts);
2598
- }
2599
- catch (err) {
2600
- console.error("[vectormind] embeddings semantic_search failed; falling back:", err);
2601
- }
3362
+ function tokenSearchInternal(opts) {
3363
+ const q = opts.query.trim();
3364
+ if (!q)
3365
+ return { query: "", top_k: opts.topK, mode: "token", matches: [] };
3366
+ const tokens = extractSearchTokens(q);
3367
+ if (!tokens.length)
3368
+ return { query: q, top_k: opts.topK, mode: "token", matches: [] };
3369
+ const rawLimit = Math.min(160, Math.max(opts.topK * 12, 80));
3370
+ const searchTokens = tokens.slice(0, 8);
3371
+ const effectiveKinds = opts.kinds?.length ? opts.kinds : TOKEN_SEARCH_DEFAULT_KINDS;
3372
+ const kindClause = effectiveKinds.length
3373
+ ? `AND kind IN (${effectiveKinds.map(() => "?").join(", ")})`
3374
+ : "";
3375
+ const recencyBoost = `
3376
+ + CASE
3377
+ WHEN updated_at >= datetime('now', '-2 days') THEN 4
3378
+ WHEN updated_at >= datetime('now', '-14 days') THEN 2
3379
+ WHEN updated_at >= datetime('now', '-60 days') THEN 1
3380
+ ELSE 0
3381
+ END`;
3382
+ const candidateScore = `
3383
+ (
3384
+ CASE kind
3385
+ WHEN 'decision' THEN 9
3386
+ WHEN 'convention' THEN 7
3387
+ WHEN 'project_summary' THEN 6
3388
+ WHEN 'note' THEN 5
3389
+ WHEN 'requirement' THEN 4
3390
+ WHEN 'change_intent' THEN 3
3391
+ ELSE 0
3392
+ END
3393
+ ${recencyBoost}
3394
+ )`;
3395
+ const includesIndexedChunks = effectiveKinds.some((k) => k === "code_chunk" || k === "doc_chunk");
3396
+ if (!includesIndexedChunks) {
3397
+ const candidateLimit = Math.min(1600, Math.max(rawLimit * 5, 800));
3398
+ const stmt = db.prepare(`
3399
+ SELECT
3400
+ id,
3401
+ kind,
3402
+ title,
3403
+ content,
3404
+ file_path,
3405
+ start_line,
3406
+ end_line,
3407
+ req_id,
3408
+ metadata_json,
3409
+ updated_at
3410
+ FROM memory_items
3411
+ WHERE 1=1
3412
+ ${kindClause}
3413
+ ORDER BY
3414
+ ${candidateScore} DESC,
3415
+ updated_at DESC,
3416
+ id DESC
3417
+ LIMIT ?
3418
+ `);
3419
+ const candidates = stmt.all(...effectiveKinds, candidateLimit);
3420
+ const scoreMap = new Map();
3421
+ const rows = candidates.filter((row) => {
3422
+ const score = tokenLexicalScore(row, q, tokens);
3423
+ if (score <= 0)
3424
+ return false;
3425
+ scoreMap.set(row.id, score);
3426
+ return true;
3427
+ });
3428
+ const matches = filterAndRankSemanticRows(rows, (r) => scoreMap.get(r.id) ?? 0, opts);
3429
+ return { query: q, top_k: opts.topK, mode: "token", matches };
3430
+ }
3431
+ const memoryFirstKinds = effectiveKinds.filter((k) => k !== "code_chunk" && k !== "doc_chunk");
3432
+ const memoryFirstLimit = Math.min(1200, Math.max(rawLimit * 4, 300));
3433
+ let memoryFirstRows = [];
3434
+ const memoryFirstScores = new Map();
3435
+ if (memoryFirstKinds.length) {
3436
+ const memoryKindClause = `AND kind IN (${memoryFirstKinds.map(() => "?").join(", ")})`;
3437
+ const memoryStmt = db.prepare(`
3438
+ SELECT
3439
+ id,
3440
+ kind,
3441
+ title,
3442
+ content,
3443
+ file_path,
3444
+ start_line,
3445
+ end_line,
3446
+ req_id,
3447
+ metadata_json,
3448
+ updated_at
3449
+ FROM memory_items
3450
+ WHERE 1=1
3451
+ ${memoryKindClause}
3452
+ ORDER BY
3453
+ ${candidateScore} DESC,
3454
+ updated_at DESC,
3455
+ id DESC
3456
+ LIMIT ?
3457
+ `);
3458
+ const candidates = memoryStmt.all(...memoryFirstKinds, memoryFirstLimit);
3459
+ memoryFirstRows = candidates.filter((row) => {
3460
+ const score = tokenLexicalScore(row, q, tokens);
3461
+ if (score <= 0)
3462
+ return false;
3463
+ memoryFirstScores.set(row.id, score);
3464
+ return true;
3465
+ });
2602
3466
  }
3467
+ const conditions = [];
3468
+ const values = [];
3469
+ for (const token of searchTokens) {
3470
+ const like = `%${escapeLike(token)}%`;
3471
+ conditions.push(`content LIKE ? ESCAPE '\\'`);
3472
+ values.push(like);
3473
+ conditions.push(`title LIKE ? ESCAPE '\\'`);
3474
+ values.push(like);
3475
+ conditions.push(`file_path LIKE ? ESCAPE '\\'`);
3476
+ values.push(like);
3477
+ }
3478
+ if (!conditions.length)
3479
+ return { query: q, top_k: opts.topK, mode: "token", matches: [] };
3480
+ const stmt = db.prepare(`
3481
+ SELECT
3482
+ id,
3483
+ kind,
3484
+ title,
3485
+ content,
3486
+ file_path,
3487
+ start_line,
3488
+ end_line,
3489
+ req_id,
3490
+ metadata_json,
3491
+ updated_at
3492
+ FROM memory_items
3493
+ WHERE (${conditions.join(" OR ")})
3494
+ ${kindClause}
3495
+ ORDER BY
3496
+ ${candidateScore} DESC,
3497
+ updated_at DESC,
3498
+ id DESC
3499
+ LIMIT ?
3500
+ `);
3501
+ const rows = stmt.all(...values, ...effectiveKinds, rawLimit);
3502
+ const scoreMap = new Map(memoryFirstScores);
3503
+ for (const row of rows) {
3504
+ if (!scoreMap.has(row.id))
3505
+ scoreMap.set(row.id, tokenLexicalScore(row, q, tokens));
3506
+ }
3507
+ const rowMap = new Map();
3508
+ for (const row of memoryFirstRows)
3509
+ rowMap.set(row.id, row);
3510
+ for (const row of rows)
3511
+ rowMap.set(row.id, row);
3512
+ const matches = filterAndRankSemanticRows(Array.from(rowMap.values()), (r) => scoreMap.get(r.id) ?? 0, opts);
3513
+ return { query: q, top_k: opts.topK, mode: "token", matches };
3514
+ }
3515
+ function chooseLexicalResult(opts) {
3516
+ const tokenResult = tokenSearchInternal(opts);
3517
+ const tokenTopScore = tokenResult.matches[0]?.score ?? 0;
3518
+ const tokenEnough = tokenResult.matches.length >= Math.min(opts.topK, 3) && tokenTopScore >= 8;
3519
+ if (tokenEnough || tokenResult.matches.length >= opts.topK) {
3520
+ return { result: tokenResult, mode: "token" };
3521
+ }
3522
+ let textResult = null;
2603
3523
  if (ftsAvailable) {
2604
3524
  try {
2605
- return ftsSearchInternal(opts);
3525
+ textResult = ftsSearchInternal(opts);
2606
3526
  }
2607
3527
  catch (err) {
2608
3528
  console.error("[vectormind] fts semantic_search failed; falling back:", err);
2609
3529
  }
2610
3530
  }
2611
- return likeSearchInternal(opts);
3531
+ if (!textResult) {
3532
+ textResult = likeSearchInternal(opts);
3533
+ }
3534
+ const merged = mergeSemanticMatches([textResult.matches, tokenResult.matches], opts);
3535
+ const tokenIds = new Set(tokenResult.matches.map((m) => m.item.id));
3536
+ const ftsKept = textResult.matches.some((m) => !tokenIds.has(m.item.id));
3537
+ if (tokenResult.matches.length && ftsKept) {
3538
+ return {
3539
+ result: { query: opts.query.trim(), top_k: opts.topK, mode: "hybrid", matches: merged },
3540
+ mode: "hybrid",
3541
+ };
3542
+ }
3543
+ if (tokenResult.matches.length) {
3544
+ return { result: { query: opts.query.trim(), top_k: opts.topK, mode: "token", matches: merged }, mode: "token" };
3545
+ }
3546
+ return { result: textResult, mode: textResult.mode === "fts" ? "fts" : "like" };
3547
+ }
3548
+ async function semanticSearchHybridInternal(opts) {
3549
+ const lexical = chooseLexicalResult(opts).result;
3550
+ if (!embeddingsEnabled)
3551
+ return lexical;
3552
+ const embeddingsResult = await Promise.race([
3553
+ semanticSearchInternal(opts),
3554
+ new Promise((resolve) => setTimeout(resolve, SEMANTIC_EMBEDDINGS_TIMEOUT_MS, null)),
3555
+ ]).catch((err) => {
3556
+ console.error("[vectormind] embeddings semantic_search failed; falling back:", err);
3557
+ return null;
3558
+ });
3559
+ if (!embeddingsResult)
3560
+ return lexical;
3561
+ const merged = mergeSemanticMatches([lexical.matches, embeddingsResult.matches], opts);
3562
+ return {
3563
+ query: opts.query.trim(),
3564
+ top_k: opts.topK,
3565
+ mode: merged.length ? "hybrid" : embeddingsResult.mode,
3566
+ matches: merged,
3567
+ };
2612
3568
  }
2613
3569
  function escapeRegExp(literal) {
2614
3570
  return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3313,7 +4269,7 @@ function listProjectFilesInternal(opts) {
3313
4269
  function buildServerInstructions() {
3314
4270
  return [
3315
4271
  "VectorMind MCP is available in this session. Use it to avoid guessing project context.",
3316
- "Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations only. They are unrelated to AI access permissions, runtime permissions, command permissions, filesystem/network permissions, approval mechanisms, or sandbox behavior.",
4272
+ "Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations.",
3317
4273
  "Project root resolution order: tool argument project_root (recommended for clients without roots/list), then VECTORMIND_ROOT (avoid hardcoding in global config), then MCP roots/list (best-effort; falls back quickly if unsupported), then process.cwd() (so start your MCP client in the project directory for per-project isolation).",
3318
4274
  "If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
3319
4275
  "",
@@ -3348,12 +4304,12 @@ function buildServerInstructions() {
3348
4304
  "- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
3349
4305
  "- On every new conversation/session for analysis/design/development work: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore compact context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
3350
4306
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
3351
- " - Tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit, preview_chars, pending_limit/pending_offset.",
4307
+ " - bootstrap_context/get_brain_dump always include a small recency anchor named current_context (latest active requirements, recent notes, and recent change intents) in addition to query matches; tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit/current_context_limit, preview_chars, pending_limit/pending_offset.",
3352
4308
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
3353
4309
  "- For pure execution-first tasks with explicit targets (for example compile/build/run/launch/package/publish/test rerun), you may skip retrieval and go straight to the minimum necessary shell or host tools unless code/context lookup is actually needed to unblock execution.",
3354
4310
  "- If rtk is installed or VectorMind's bundled RTK shim is verified (detect_rtk with gain_ok=true), prefix shell commands with the command returned by detect_rtk. Usually this is rtk (rtk git status, rtk npm run build, rtk rg ...); in npx/MCP-only installs it may be a package shim command such as node <...>/rtk-shim.js.",
3355
- "- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only after the user clearly approves installation/init choices.",
3356
- "- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming a filesystem MCP resource server exists.",
4311
+ "- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only when the user explicitly asks to install/init.",
4312
+ "- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming another local-file MCP resource server exists.",
3357
4313
  "- For project file/directory browsing, prefer list_project_files({ path, recursive?, max_depth? }) over shelling out to Get-ChildItem/ls. It respects ignore rules and keeps output bounded.",
3358
4314
  "- For small/medium raw file reads, prefer read_file_text({ path, offset?, max_chars? }) over Get-Content -Raw. Use read_file_lines(...) when you need deterministic line ranges or the file may be large.",
3359
4315
  "- For raw repo text search with exact file+line+col matches, prefer grep({ query: <pattern> }). It uses ripgrep against real project files when available, applies built-in noise filters, and only falls back to indexed search if ripgrep is unavailable.",
@@ -3365,7 +4321,8 @@ function buildServerInstructions() {
3365
4321
  "- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
3366
4322
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
3367
4323
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
3368
- "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
4324
+ "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing. It blends lexical/FTS recall with embeddings when enabled, so recent explicit wording and durable decisions are not hidden by older semantically similar matches.",
4325
+ "- VectorMind automatically runs small, throttled memory maintenance to compact old completed history and prune stale indexes in long-lived projects. For large repos that feel slow, call maintain_memory({ dry_run: true }) first, then maintain_memory({ dry_run: false }) if the plan looks correct.",
3369
4326
  "- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
3370
4327
  "",
3371
4328
  "If tool output conflicts with assumptions, trust the tool output.",
@@ -3483,6 +4440,15 @@ function initMemoryItemsFts() {
3483
4440
  ftsAvailable = false;
3484
4441
  }
3485
4442
  }
4443
+ function columnExists(table, column) {
4444
+ try {
4445
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
4446
+ return rows.some((row) => row.name === column);
4447
+ }
4448
+ catch {
4449
+ return false;
4450
+ }
4451
+ }
3486
4452
  function initDatabase() {
3487
4453
  const vmDir = path.join(projectRoot, ".vectormind");
3488
4454
  try {
@@ -3535,7 +4501,8 @@ function initDatabase() {
3535
4501
  title TEXT NOT NULL,
3536
4502
  status TEXT DEFAULT 'active',
3537
4503
  context_data TEXT,
3538
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
4504
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
4505
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
3539
4506
  );
3540
4507
 
3541
4508
  CREATE TABLE IF NOT EXISTS change_logs (
@@ -3634,6 +4601,49 @@ function initDatabase() {
3634
4601
 
3635
4602
  CREATE INDEX IF NOT EXISTS idx_token_savings_tool
3636
4603
  ON token_savings(tool);
4604
+
4605
+ CREATE TABLE IF NOT EXISTS memory_item_archive (
4606
+ memory_id INTEGER PRIMARY KEY,
4607
+ original_kind TEXT NOT NULL,
4608
+ original_title TEXT,
4609
+ original_content TEXT NOT NULL,
4610
+ original_file_path TEXT,
4611
+ original_start_line INTEGER,
4612
+ original_end_line INTEGER,
4613
+ original_req_id INTEGER,
4614
+ original_metadata_json TEXT,
4615
+ original_content_hash TEXT,
4616
+ original_created_at DATETIME,
4617
+ original_updated_at DATETIME,
4618
+ archive_reason TEXT NOT NULL,
4619
+ compacted_into_id INTEGER,
4620
+ archived_at DATETIME DEFAULT CURRENT_TIMESTAMP
4621
+ );
4622
+
4623
+ CREATE INDEX IF NOT EXISTS idx_memory_item_archive_compacted_into
4624
+ ON memory_item_archive(compacted_into_id);
4625
+
4626
+ CREATE TABLE IF NOT EXISTS meta_kv (
4627
+ key TEXT PRIMARY KEY,
4628
+ value TEXT NOT NULL,
4629
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
4630
+ );
4631
+ `);
4632
+ if (!columnExists("requirements", "updated_at")) {
4633
+ db.exec(`ALTER TABLE requirements ADD COLUMN updated_at DATETIME`);
4634
+ db.exec(`UPDATE requirements SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL`);
4635
+ }
4636
+ db.exec(`
4637
+ UPDATE requirements SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL;
4638
+ CREATE INDEX IF NOT EXISTS idx_requirements_status_updated_at
4639
+ ON requirements(status, updated_at DESC, id DESC);
4640
+ CREATE TRIGGER IF NOT EXISTS vectormind_requirements_touch_updated_at
4641
+ AFTER UPDATE ON requirements
4642
+ FOR EACH ROW
4643
+ WHEN NEW.updated_at = OLD.updated_at
4644
+ BEGIN
4645
+ UPDATE requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
4646
+ END;
3637
4647
  `);
3638
4648
  initMemoryItemsFts();
3639
4649
  insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
@@ -3642,11 +4652,16 @@ function initDatabase() {
3642
4652
  getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, created_at
3643
4653
  FROM requirements
3644
4654
  WHERE status = 'active'
3645
- ORDER BY created_at DESC, id DESC
4655
+ ORDER BY updated_at DESC, created_at DESC, id DESC
3646
4656
  LIMIT 1`);
4657
+ listActiveRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
4658
+ FROM requirements
4659
+ WHERE status = 'active'
4660
+ ORDER BY updated_at DESC, created_at DESC, id DESC
4661
+ LIMIT ?`);
3647
4662
  listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
3648
4663
  FROM requirements
3649
- ORDER BY created_at DESC, id DESC
4664
+ ORDER BY updated_at DESC, created_at DESC, id DESC
3650
4665
  LIMIT ?`);
3651
4666
  completeAllActiveRequirementMemoryItemsStmt = db.prepare(`UPDATE memory_items
3652
4667
  SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP
@@ -3723,6 +4738,11 @@ function initDatabase() {
3723
4738
  WHERE kind = 'note'
3724
4739
  ORDER BY updated_at DESC, id DESC
3725
4740
  LIMIT ?`);
4741
+ listRecentContextItemsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
4742
+ FROM memory_items
4743
+ WHERE kind IN ('note', 'requirement', 'change_intent')
4744
+ ORDER BY updated_at DESC, id DESC
4745
+ LIMIT ?`);
3726
4746
  getLatestChangeIntentForFileStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3727
4747
  FROM memory_items
3728
4748
  WHERE kind = 'change_intent' AND file_path = ?
@@ -3801,6 +4821,10 @@ function initDatabase() {
3801
4821
  FROM token_savings
3802
4822
  ORDER BY created_at DESC, id DESC
3803
4823
  LIMIT ?`);
4824
+ getKvStmt = db.prepare(`SELECT value FROM meta_kv WHERE key = ?`);
4825
+ setKvStmt = db.prepare(`INSERT INTO meta_kv (key, value)
4826
+ VALUES (?, ?)
4827
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`);
3804
4828
  indexFileSymbolsTx = db.transaction((filePath, symbols) => {
3805
4829
  deleteSymbolsForFileStmt.run(filePath);
3806
4830
  for (const s of symbols) {
@@ -3816,6 +4840,9 @@ function initDatabase() {
3816
4840
  // Clean up common "file name noise" recorded by older versions.
3817
4841
  // (These files are ignored by current index rules; keep the DB consistent automatically.)
3818
4842
  pruneFilenameNoiseIndexes();
4843
+ // Bounded, throttled maintenance keeps long-lived project memory fast without
4844
+ // deleting durable decisions/conventions/project summaries.
4845
+ runAutoMaintenanceIfDue();
3819
4846
  }
3820
4847
  function initWatcher() {
3821
4848
  watcherReady = false;
@@ -4022,7 +5049,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4022
5049
  },
4023
5050
  {
4024
5051
  name: "read_codex_text_file",
4025
- description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming a filesystem MCP resource server exists.",
5052
+ description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming another local-file MCP resource server exists.",
4026
5053
  inputSchema: toJsonSchemaCompat(ReadCodexTextFileArgsSchema),
4027
5054
  },
4028
5055
  {
@@ -4070,6 +5097,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4070
5097
  description: "Semantic search across the local memory store (requirements, change intents, notes, project summary, and indexed code/doc chunks). Use this to retrieve relevant context instead of guessing.",
4071
5098
  inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
4072
5099
  },
5100
+ {
5101
+ name: "maintain_memory",
5102
+ description: "Compact old completed memory and prune stale/noisy indexes to keep long-lived large projects fast. Defaults to dry_run=true; automatic safe maintenance also runs periodically.",
5103
+ inputSchema: toJsonSchemaCompat(MaintainMemoryArgsSchema),
5104
+ },
4073
5105
  {
4074
5106
  name: "prune_index",
4075
5107
  description: "Prune noisy auto-indexed items (code_chunk/doc_chunk + symbols). Useful after tightening ignore rules to shrink the index and improve search relevance.",
@@ -4257,6 +5289,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4257
5289
  ],
4258
5290
  };
4259
5291
  }
5292
+ if (toolName === "maintain_memory") {
5293
+ const args = MaintainMemoryArgsSchema.parse(rawArgs);
5294
+ flushPendingChangeBuffer();
5295
+ const result = runMemoryMaintenance(args, "manual");
5296
+ return {
5297
+ content: [
5298
+ {
5299
+ type: "text",
5300
+ text: toolCompactOrJson("maintain_memory", result, compactMaintenanceText(result), args.format),
5301
+ },
5302
+ ],
5303
+ };
5304
+ }
4260
5305
  if (toolName === "sync_change_intent") {
4261
5306
  const args = SyncChangeIntentArgsSchema.parse(rawArgs);
4262
5307
  flushPendingChangeBuffer();
@@ -4373,6 +5418,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4373
5418
  const notesLimit = args.notes_limit;
4374
5419
  const conventionsLimit = args.conventions_limit;
4375
5420
  const decisionsLimit = args.decisions_limit;
5421
+ const currentContextLimit = args.current_context_limit;
4376
5422
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4377
5423
  const items = recent.map((req) => {
4378
5424
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4388,6 +5434,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4388
5434
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4389
5435
  const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4390
5436
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
5437
+ const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
4391
5438
  const pending_offset = args.pending_offset;
4392
5439
  const pending_limit = args.pending_limit;
4393
5440
  const pendingDbRows = listPendingChangesStmt.all();
@@ -4396,12 +5443,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4396
5443
  const pending_truncated = mergedPending.truncated;
4397
5444
  const pending_changes = mergedPending.page;
4398
5445
  const q = args.query?.trim() ?? "";
5446
+ const semanticKinds = args.kinds?.length ? args.kinds : BOOTSTRAP_DEFAULT_CONTEXT_KINDS;
4399
5447
  const semantic = q
4400
5448
  ? await Promise.race([
4401
5449
  semanticSearchHybridInternal({
4402
5450
  query: q,
4403
5451
  topK: args.top_k,
4404
- kinds: args.kinds?.length ? args.kinds : null,
5452
+ kinds: semanticKinds,
4405
5453
  includeContent,
4406
5454
  previewChars,
4407
5455
  contentMaxChars,
@@ -4418,6 +5466,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4418
5466
  pending_returned: pending_changes.length,
4419
5467
  requirements_returned: items.length,
4420
5468
  decisions_returned: decisions.length,
5469
+ current_context_returned: current_context.length,
4421
5470
  conventions_returned: conventions.length,
4422
5471
  semantic_mode: semantic?.mode ?? null,
4423
5472
  semantic_matches: semantic?.matches?.length ?? 0,
@@ -4444,11 +5493,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4444
5493
  changes_limit: changesLimit,
4445
5494
  notes_limit: notesLimit,
4446
5495
  decisions_limit: decisionsLimit,
5496
+ current_context_limit: currentContextLimit,
4447
5497
  conventions_limit: conventionsLimit,
4448
5498
  },
4449
5499
  project_summary,
4450
5500
  decisions,
4451
5501
  conventions,
5502
+ current_context,
4452
5503
  recent_notes,
4453
5504
  pending_total,
4454
5505
  pending_offset,
@@ -4478,6 +5529,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4478
5529
  const notesLimit = args.notes_limit;
4479
5530
  const conventionsLimit = args.conventions_limit;
4480
5531
  const decisionsLimit = args.decisions_limit;
5532
+ const currentContextLimit = args.current_context_limit;
4481
5533
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4482
5534
  const items = recent.map((req) => {
4483
5535
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4493,6 +5545,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4493
5545
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4494
5546
  const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4495
5547
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
5548
+ const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
4496
5549
  const pending_offset = args.pending_offset;
4497
5550
  const pending_limit = args.pending_limit;
4498
5551
  const pendingDbRows = listPendingChangesStmt.all();
@@ -4506,6 +5559,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4506
5559
  requirements_returned: items.length,
4507
5560
  notes_returned: recent_notes.length,
4508
5561
  decisions_returned: decisions.length,
5562
+ current_context_returned: current_context.length,
4509
5563
  conventions_returned: conventions.length,
4510
5564
  });
4511
5565
  const outputValue = {
@@ -4530,11 +5584,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4530
5584
  changes_limit: changesLimit,
4531
5585
  notes_limit: notesLimit,
4532
5586
  decisions_limit: decisionsLimit,
5587
+ current_context_limit: currentContextLimit,
4533
5588
  conventions_limit: conventionsLimit,
4534
5589
  },
4535
5590
  project_summary,
4536
5591
  decisions,
4537
5592
  conventions,
5593
+ current_context,
4538
5594
  recent_notes,
4539
5595
  pending_total,
4540
5596
  pending_offset,