@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/README.md +376 -384
- package/dist/builtin-conventions.js +26 -135
- package/dist/builtin-conventions.js.map +1 -1
- package/dist/builtin-instructions.d.ts +9 -9
- package/dist/builtin-instructions.js +10 -13
- package/dist/builtin-instructions.js.map +1 -1
- package/dist/index.js +1098 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 === "
|
|
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 (
|
|
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) => !
|
|
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 (
|
|
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
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
" -
|
|
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
|
|
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
|
|
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
|
-
"-
|
|
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
|
|
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:
|
|
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,
|