@coreyuan/vector-mind 1.0.41 → 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.41";
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,57 +4269,51 @@ 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
- "This package ships built-in baseline policy. If a client supports MCP instructions, these rules auto-apply as soon as the MCP is installed and connected; no user-side config file is required.",
3317
- "The write-operation rules below are strict workflow constraints. Do not claim that the environment has real git branch locks, checkout APIs, or file-lock tools unless such tools are actually available in the current client/runtime. If such tools are absent, you must still enforce the same exclusivity semantics through explicit coordination and serialized same-file edits.",
4272
+ "Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations.",
3318
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).",
3319
4274
  "If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
3320
4275
  "",
3321
- "Built-in write-operation policy:",
4276
+ "Built-in write-operation quality policy:",
3322
4277
  BUILTIN_WRITE_POLICY_INSTRUCTIONS,
3323
4278
  "",
3324
- "Built-in task-list / Plan-Lite policy:",
4279
+ "Built-in task-list / Plan-Lite quality policy:",
3325
4280
  BUILTIN_PLAN_LITE_INSTRUCTIONS,
3326
4281
  "",
3327
- "Built-in destructive-operation guard policy:",
4282
+ "Built-in destructive-operation quality guard:",
3328
4283
  BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS,
3329
4284
  "",
3330
- "Built-in architecture and code-organization policy:",
4285
+ "Built-in architecture and code-organization quality policy:",
3331
4286
  BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
3332
4287
  "",
3333
- "Built-in frontend output-purity policy:",
4288
+ "Built-in frontend output-purity quality policy:",
3334
4289
  BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS,
3335
4290
  "",
3336
- "Built-in git commit summary policy:",
4291
+ "Built-in git commit summary quality policy:",
3337
4292
  BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS,
3338
4293
  "",
3339
- "Built-in low-overhead execution and heavy-thread policy:",
4294
+ "Built-in low-overhead execution and heavy-thread quality policy:",
3340
4295
  BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS,
3341
4296
  "",
3342
- "Built-in payload / oversized-thread guard policy:",
4297
+ "Built-in payload / oversized-thread quality guard:",
3343
4298
  BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS,
3344
4299
  "",
3345
- "Built-in thread handoff / switch-gate policy:",
4300
+ "Built-in thread handoff / switch-gate quality policy:",
3346
4301
  BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS,
3347
4302
  "",
3348
- "Required workflow:",
4303
+ "VectorMind workflow:",
3349
4304
  "- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
3350
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).",
3351
4306
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
3352
- " - 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.",
3353
4308
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
3354
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.",
3355
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.",
3356
- "- 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.",
3357
- "- 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.",
3358
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.",
3359
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.",
3360
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.",
3361
4316
  "- To read a bounded segment of a file, prefer read_file_lines({ path: <file>, from_line/to_line or total_count }) over unbounded file reads.",
3362
- "- If the current thread is heavy, recently compacted, has become slow, or has already hit a 413 / Payload Too Large style error, switch to payload guard mode: avoid unbounded shell dumps, prefer bounded MCP tools, and summarize outputs instead of pasting large raw blocks.",
3363
- "- In payload guard mode, do not use full-repo recursive listings, whole-file dumps, or broad raw match echo unless the user explicitly requests that raw output and accepts the size risk.",
3364
- "- Thread-switch judgment must not rely on a fixed token threshold; use observable signals plus the weight of the upcoming work. If the current thread is heavy, repeatedly compacting, slow, or has already hit a 413 / Payload Too Large style error and the next work still needs broad analysis, cross-module investigation, release validation, or other substantial continuation, pause once and ask whether to switch to a fresh thread before continuing.",
3365
- "- If the user declines that switch, continue in the current thread and do not raise the thread-switch reminder again in the same session.",
3366
- "- If the user accepts, or explicitly asks you to pack the current conversation for a new thread, do not attempt to create a new thread on the user's behalf and do not claim that you already created one. Use add_note(...) to persist a concise handoff note, include other relevant existing note ids only when they are truly needed, then reply briefly with the new handoff note id and tell the user that in the new thread they can say '读取 note <id> [和 note <id>] 继续'.",
3367
4317
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
3368
4318
  "- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
3369
4319
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
@@ -3371,8 +4321,8 @@ function buildServerInstructions() {
3371
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.",
3372
4322
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
3373
4323
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
3374
- "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
3375
- "- If the current thread is already heavy or the user reports it has become slow, switch to a lighter workflow: avoid redundant retrieval, keep outputs compact, and if the user refuses thread switching, continue in light mode without repeating the switch reminder in that same session.",
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.",
3376
4326
  "- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
3377
4327
  "",
3378
4328
  "If tool output conflicts with assumptions, trust the tool output.",
@@ -3490,6 +4440,15 @@ function initMemoryItemsFts() {
3490
4440
  ftsAvailable = false;
3491
4441
  }
3492
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
+ }
3493
4452
  function initDatabase() {
3494
4453
  const vmDir = path.join(projectRoot, ".vectormind");
3495
4454
  try {
@@ -3542,7 +4501,8 @@ function initDatabase() {
3542
4501
  title TEXT NOT NULL,
3543
4502
  status TEXT DEFAULT 'active',
3544
4503
  context_data TEXT,
3545
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
4504
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
4505
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
3546
4506
  );
3547
4507
 
3548
4508
  CREATE TABLE IF NOT EXISTS change_logs (
@@ -3641,6 +4601,49 @@ function initDatabase() {
3641
4601
 
3642
4602
  CREATE INDEX IF NOT EXISTS idx_token_savings_tool
3643
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;
3644
4647
  `);
3645
4648
  initMemoryItemsFts();
3646
4649
  insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
@@ -3649,11 +4652,16 @@ function initDatabase() {
3649
4652
  getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, created_at
3650
4653
  FROM requirements
3651
4654
  WHERE status = 'active'
3652
- ORDER BY created_at DESC, id DESC
4655
+ ORDER BY updated_at DESC, created_at DESC, id DESC
3653
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 ?`);
3654
4662
  listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
3655
4663
  FROM requirements
3656
- ORDER BY created_at DESC, id DESC
4664
+ ORDER BY updated_at DESC, created_at DESC, id DESC
3657
4665
  LIMIT ?`);
3658
4666
  completeAllActiveRequirementMemoryItemsStmt = db.prepare(`UPDATE memory_items
3659
4667
  SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP
@@ -3730,6 +4738,11 @@ function initDatabase() {
3730
4738
  WHERE kind = 'note'
3731
4739
  ORDER BY updated_at DESC, id DESC
3732
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 ?`);
3733
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
3734
4747
  FROM memory_items
3735
4748
  WHERE kind = 'change_intent' AND file_path = ?
@@ -3808,6 +4821,10 @@ function initDatabase() {
3808
4821
  FROM token_savings
3809
4822
  ORDER BY created_at DESC, id DESC
3810
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`);
3811
4828
  indexFileSymbolsTx = db.transaction((filePath, symbols) => {
3812
4829
  deleteSymbolsForFileStmt.run(filePath);
3813
4830
  for (const s of symbols) {
@@ -3823,6 +4840,9 @@ function initDatabase() {
3823
4840
  // Clean up common "file name noise" recorded by older versions.
3824
4841
  // (These files are ignored by current index rules; keep the DB consistent automatically.)
3825
4842
  pruneFilenameNoiseIndexes();
4843
+ // Bounded, throttled maintenance keeps long-lived project memory fast without
4844
+ // deleting durable decisions/conventions/project summaries.
4845
+ runAutoMaintenanceIfDue();
3826
4846
  }
3827
4847
  function initWatcher() {
3828
4848
  watcherReady = false;
@@ -4029,7 +5049,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4029
5049
  },
4030
5050
  {
4031
5051
  name: "read_codex_text_file",
4032
- 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.",
4033
5053
  inputSchema: toJsonSchemaCompat(ReadCodexTextFileArgsSchema),
4034
5054
  },
4035
5055
  {
@@ -4077,6 +5097,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4077
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.",
4078
5098
  inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
4079
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
+ },
4080
5105
  {
4081
5106
  name: "prune_index",
4082
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.",
@@ -4264,6 +5289,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4264
5289
  ],
4265
5290
  };
4266
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
+ }
4267
5305
  if (toolName === "sync_change_intent") {
4268
5306
  const args = SyncChangeIntentArgsSchema.parse(rawArgs);
4269
5307
  flushPendingChangeBuffer();
@@ -4380,6 +5418,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4380
5418
  const notesLimit = args.notes_limit;
4381
5419
  const conventionsLimit = args.conventions_limit;
4382
5420
  const decisionsLimit = args.decisions_limit;
5421
+ const currentContextLimit = args.current_context_limit;
4383
5422
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4384
5423
  const items = recent.map((req) => {
4385
5424
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4395,6 +5434,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4395
5434
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4396
5435
  const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4397
5436
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
5437
+ const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
4398
5438
  const pending_offset = args.pending_offset;
4399
5439
  const pending_limit = args.pending_limit;
4400
5440
  const pendingDbRows = listPendingChangesStmt.all();
@@ -4403,12 +5443,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4403
5443
  const pending_truncated = mergedPending.truncated;
4404
5444
  const pending_changes = mergedPending.page;
4405
5445
  const q = args.query?.trim() ?? "";
5446
+ const semanticKinds = args.kinds?.length ? args.kinds : BOOTSTRAP_DEFAULT_CONTEXT_KINDS;
4406
5447
  const semantic = q
4407
5448
  ? await Promise.race([
4408
5449
  semanticSearchHybridInternal({
4409
5450
  query: q,
4410
5451
  topK: args.top_k,
4411
- kinds: args.kinds?.length ? args.kinds : null,
5452
+ kinds: semanticKinds,
4412
5453
  includeContent,
4413
5454
  previewChars,
4414
5455
  contentMaxChars,
@@ -4425,6 +5466,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4425
5466
  pending_returned: pending_changes.length,
4426
5467
  requirements_returned: items.length,
4427
5468
  decisions_returned: decisions.length,
5469
+ current_context_returned: current_context.length,
4428
5470
  conventions_returned: conventions.length,
4429
5471
  semantic_mode: semantic?.mode ?? null,
4430
5472
  semantic_matches: semantic?.matches?.length ?? 0,
@@ -4451,11 +5493,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4451
5493
  changes_limit: changesLimit,
4452
5494
  notes_limit: notesLimit,
4453
5495
  decisions_limit: decisionsLimit,
5496
+ current_context_limit: currentContextLimit,
4454
5497
  conventions_limit: conventionsLimit,
4455
5498
  },
4456
5499
  project_summary,
4457
5500
  decisions,
4458
5501
  conventions,
5502
+ current_context,
4459
5503
  recent_notes,
4460
5504
  pending_total,
4461
5505
  pending_offset,
@@ -4485,6 +5529,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4485
5529
  const notesLimit = args.notes_limit;
4486
5530
  const conventionsLimit = args.conventions_limit;
4487
5531
  const decisionsLimit = args.decisions_limit;
5532
+ const currentContextLimit = args.current_context_limit;
4488
5533
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4489
5534
  const items = recent.map((req) => {
4490
5535
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4500,6 +5545,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4500
5545
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4501
5546
  const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4502
5547
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
5548
+ const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
4503
5549
  const pending_offset = args.pending_offset;
4504
5550
  const pending_limit = args.pending_limit;
4505
5551
  const pendingDbRows = listPendingChangesStmt.all();
@@ -4513,6 +5559,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4513
5559
  requirements_returned: items.length,
4514
5560
  notes_returned: recent_notes.length,
4515
5561
  decisions_returned: decisions.length,
5562
+ current_context_returned: current_context.length,
4516
5563
  conventions_returned: conventions.length,
4517
5564
  });
4518
5565
  const outputValue = {
@@ -4537,11 +5584,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4537
5584
  changes_limit: changesLimit,
4538
5585
  notes_limit: notesLimit,
4539
5586
  decisions_limit: decisionsLimit,
5587
+ current_context_limit: currentContextLimit,
4540
5588
  conventions_limit: conventionsLimit,
4541
5589
  },
4542
5590
  project_summary,
4543
5591
  decisions,
4544
5592
  conventions,
5593
+ current_context,
4545
5594
  recent_notes,
4546
5595
  pending_total,
4547
5596
  pending_offset,