@coreyuan/vector-mind 1.0.42 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -361
- package/dist/builtin-conventions.js +2 -2
- package/dist/builtin-conventions.js.map +1 -1
- package/dist/builtin-instructions.d.ts +9 -9
- package/dist/builtin-instructions.js +1 -1
- package/dist/builtin-instructions.js.map +1 -1
- package/dist/index.js +1088 -32
- 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,7 +4269,7 @@ function listProjectFilesInternal(opts) {
|
|
|
3313
4269
|
function buildServerInstructions() {
|
|
3314
4270
|
return [
|
|
3315
4271
|
"VectorMind MCP is available in this session. Use it to avoid guessing project context.",
|
|
3316
|
-
"Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations
|
|
4272
|
+
"Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations.",
|
|
3317
4273
|
"Project root resolution order: tool argument project_root (recommended for clients without roots/list), then VECTORMIND_ROOT (avoid hardcoding in global config), then MCP roots/list (best-effort; falls back quickly if unsupported), then process.cwd() (so start your MCP client in the project directory for per-project isolation).",
|
|
3318
4274
|
"If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
|
|
3319
4275
|
"",
|
|
@@ -3348,12 +4304,12 @@ function buildServerInstructions() {
|
|
|
3348
4304
|
"- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
|
|
3349
4305
|
"- On every new conversation/session for analysis/design/development work: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore compact context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
|
|
3350
4306
|
" - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
|
|
3351
|
-
" -
|
|
4307
|
+
" - bootstrap_context/get_brain_dump always include a small recency anchor named current_context (latest active requirements, recent notes, and recent change intents) in addition to query matches; tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit/current_context_limit, preview_chars, pending_limit/pending_offset.",
|
|
3352
4308
|
" - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
|
|
3353
4309
|
"- For pure execution-first tasks with explicit targets (for example compile/build/run/launch/package/publish/test rerun), you may skip retrieval and go straight to the minimum necessary shell or host tools unless code/context lookup is actually needed to unblock execution.",
|
|
3354
4310
|
"- If rtk is installed or VectorMind's bundled RTK shim is verified (detect_rtk with gain_ok=true), prefix shell commands with the command returned by detect_rtk. Usually this is rtk (rtk git status, rtk npm run build, rtk rg ...); in npx/MCP-only installs it may be a package shim command such as node <...>/rtk-shim.js.",
|
|
3355
|
-
"- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only
|
|
3356
|
-
"- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming
|
|
4311
|
+
"- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only when the user explicitly asks to install/init.",
|
|
4312
|
+
"- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming another local-file MCP resource server exists.",
|
|
3357
4313
|
"- For project file/directory browsing, prefer list_project_files({ path, recursive?, max_depth? }) over shelling out to Get-ChildItem/ls. It respects ignore rules and keeps output bounded.",
|
|
3358
4314
|
"- For small/medium raw file reads, prefer read_file_text({ path, offset?, max_chars? }) over Get-Content -Raw. Use read_file_lines(...) when you need deterministic line ranges or the file may be large.",
|
|
3359
4315
|
"- For raw repo text search with exact file+line+col matches, prefer grep({ query: <pattern> }). It uses ripgrep against real project files when available, applies built-in noise filters, and only falls back to indexed search if ripgrep is unavailable.",
|
|
@@ -3365,7 +4321,8 @@ function buildServerInstructions() {
|
|
|
3365
4321
|
"- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
|
|
3366
4322
|
"- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
|
|
3367
4323
|
"- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
|
|
3368
|
-
"- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
|
|
4324
|
+
"- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing. It blends lexical/FTS recall with embeddings when enabled, so recent explicit wording and durable decisions are not hidden by older semantically similar matches.",
|
|
4325
|
+
"- VectorMind automatically runs small, throttled memory maintenance to compact old completed history and prune stale indexes in long-lived projects. For large repos that feel slow, call maintain_memory({ dry_run: true }) first, then maintain_memory({ dry_run: false }) if the plan looks correct.",
|
|
3369
4326
|
"- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
|
|
3370
4327
|
"",
|
|
3371
4328
|
"If tool output conflicts with assumptions, trust the tool output.",
|
|
@@ -3483,6 +4440,15 @@ function initMemoryItemsFts() {
|
|
|
3483
4440
|
ftsAvailable = false;
|
|
3484
4441
|
}
|
|
3485
4442
|
}
|
|
4443
|
+
function columnExists(table, column) {
|
|
4444
|
+
try {
|
|
4445
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
4446
|
+
return rows.some((row) => row.name === column);
|
|
4447
|
+
}
|
|
4448
|
+
catch {
|
|
4449
|
+
return false;
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
3486
4452
|
function initDatabase() {
|
|
3487
4453
|
const vmDir = path.join(projectRoot, ".vectormind");
|
|
3488
4454
|
try {
|
|
@@ -3535,7 +4501,8 @@ function initDatabase() {
|
|
|
3535
4501
|
title TEXT NOT NULL,
|
|
3536
4502
|
status TEXT DEFAULT 'active',
|
|
3537
4503
|
context_data TEXT,
|
|
3538
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
4504
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
4505
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
3539
4506
|
);
|
|
3540
4507
|
|
|
3541
4508
|
CREATE TABLE IF NOT EXISTS change_logs (
|
|
@@ -3634,6 +4601,49 @@ function initDatabase() {
|
|
|
3634
4601
|
|
|
3635
4602
|
CREATE INDEX IF NOT EXISTS idx_token_savings_tool
|
|
3636
4603
|
ON token_savings(tool);
|
|
4604
|
+
|
|
4605
|
+
CREATE TABLE IF NOT EXISTS memory_item_archive (
|
|
4606
|
+
memory_id INTEGER PRIMARY KEY,
|
|
4607
|
+
original_kind TEXT NOT NULL,
|
|
4608
|
+
original_title TEXT,
|
|
4609
|
+
original_content TEXT NOT NULL,
|
|
4610
|
+
original_file_path TEXT,
|
|
4611
|
+
original_start_line INTEGER,
|
|
4612
|
+
original_end_line INTEGER,
|
|
4613
|
+
original_req_id INTEGER,
|
|
4614
|
+
original_metadata_json TEXT,
|
|
4615
|
+
original_content_hash TEXT,
|
|
4616
|
+
original_created_at DATETIME,
|
|
4617
|
+
original_updated_at DATETIME,
|
|
4618
|
+
archive_reason TEXT NOT NULL,
|
|
4619
|
+
compacted_into_id INTEGER,
|
|
4620
|
+
archived_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
4621
|
+
);
|
|
4622
|
+
|
|
4623
|
+
CREATE INDEX IF NOT EXISTS idx_memory_item_archive_compacted_into
|
|
4624
|
+
ON memory_item_archive(compacted_into_id);
|
|
4625
|
+
|
|
4626
|
+
CREATE TABLE IF NOT EXISTS meta_kv (
|
|
4627
|
+
key TEXT PRIMARY KEY,
|
|
4628
|
+
value TEXT NOT NULL,
|
|
4629
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
4630
|
+
);
|
|
4631
|
+
`);
|
|
4632
|
+
if (!columnExists("requirements", "updated_at")) {
|
|
4633
|
+
db.exec(`ALTER TABLE requirements ADD COLUMN updated_at DATETIME`);
|
|
4634
|
+
db.exec(`UPDATE requirements SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL`);
|
|
4635
|
+
}
|
|
4636
|
+
db.exec(`
|
|
4637
|
+
UPDATE requirements SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL;
|
|
4638
|
+
CREATE INDEX IF NOT EXISTS idx_requirements_status_updated_at
|
|
4639
|
+
ON requirements(status, updated_at DESC, id DESC);
|
|
4640
|
+
CREATE TRIGGER IF NOT EXISTS vectormind_requirements_touch_updated_at
|
|
4641
|
+
AFTER UPDATE ON requirements
|
|
4642
|
+
FOR EACH ROW
|
|
4643
|
+
WHEN NEW.updated_at = OLD.updated_at
|
|
4644
|
+
BEGIN
|
|
4645
|
+
UPDATE requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
4646
|
+
END;
|
|
3637
4647
|
`);
|
|
3638
4648
|
initMemoryItemsFts();
|
|
3639
4649
|
insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
|
|
@@ -3642,11 +4652,16 @@ function initDatabase() {
|
|
|
3642
4652
|
getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, created_at
|
|
3643
4653
|
FROM requirements
|
|
3644
4654
|
WHERE status = 'active'
|
|
3645
|
-
ORDER BY created_at DESC, id DESC
|
|
4655
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
3646
4656
|
LIMIT 1`);
|
|
4657
|
+
listActiveRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
|
|
4658
|
+
FROM requirements
|
|
4659
|
+
WHERE status = 'active'
|
|
4660
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
4661
|
+
LIMIT ?`);
|
|
3647
4662
|
listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
|
|
3648
4663
|
FROM requirements
|
|
3649
|
-
ORDER BY created_at DESC, id DESC
|
|
4664
|
+
ORDER BY updated_at DESC, created_at DESC, id DESC
|
|
3650
4665
|
LIMIT ?`);
|
|
3651
4666
|
completeAllActiveRequirementMemoryItemsStmt = db.prepare(`UPDATE memory_items
|
|
3652
4667
|
SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP
|
|
@@ -3723,6 +4738,11 @@ function initDatabase() {
|
|
|
3723
4738
|
WHERE kind = 'note'
|
|
3724
4739
|
ORDER BY updated_at DESC, id DESC
|
|
3725
4740
|
LIMIT ?`);
|
|
4741
|
+
listRecentContextItemsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
|
|
4742
|
+
FROM memory_items
|
|
4743
|
+
WHERE kind IN ('note', 'requirement', 'change_intent')
|
|
4744
|
+
ORDER BY updated_at DESC, id DESC
|
|
4745
|
+
LIMIT ?`);
|
|
3726
4746
|
getLatestChangeIntentForFileStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
|
|
3727
4747
|
FROM memory_items
|
|
3728
4748
|
WHERE kind = 'change_intent' AND file_path = ?
|
|
@@ -3801,6 +4821,10 @@ function initDatabase() {
|
|
|
3801
4821
|
FROM token_savings
|
|
3802
4822
|
ORDER BY created_at DESC, id DESC
|
|
3803
4823
|
LIMIT ?`);
|
|
4824
|
+
getKvStmt = db.prepare(`SELECT value FROM meta_kv WHERE key = ?`);
|
|
4825
|
+
setKvStmt = db.prepare(`INSERT INTO meta_kv (key, value)
|
|
4826
|
+
VALUES (?, ?)
|
|
4827
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`);
|
|
3804
4828
|
indexFileSymbolsTx = db.transaction((filePath, symbols) => {
|
|
3805
4829
|
deleteSymbolsForFileStmt.run(filePath);
|
|
3806
4830
|
for (const s of symbols) {
|
|
@@ -3816,6 +4840,9 @@ function initDatabase() {
|
|
|
3816
4840
|
// Clean up common "file name noise" recorded by older versions.
|
|
3817
4841
|
// (These files are ignored by current index rules; keep the DB consistent automatically.)
|
|
3818
4842
|
pruneFilenameNoiseIndexes();
|
|
4843
|
+
// Bounded, throttled maintenance keeps long-lived project memory fast without
|
|
4844
|
+
// deleting durable decisions/conventions/project summaries.
|
|
4845
|
+
runAutoMaintenanceIfDue();
|
|
3819
4846
|
}
|
|
3820
4847
|
function initWatcher() {
|
|
3821
4848
|
watcherReady = false;
|
|
@@ -4022,7 +5049,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4022
5049
|
},
|
|
4023
5050
|
{
|
|
4024
5051
|
name: "read_codex_text_file",
|
|
4025
|
-
description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming
|
|
5052
|
+
description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming another local-file MCP resource server exists.",
|
|
4026
5053
|
inputSchema: toJsonSchemaCompat(ReadCodexTextFileArgsSchema),
|
|
4027
5054
|
},
|
|
4028
5055
|
{
|
|
@@ -4070,6 +5097,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
4070
5097
|
description: "Semantic search across the local memory store (requirements, change intents, notes, project summary, and indexed code/doc chunks). Use this to retrieve relevant context instead of guessing.",
|
|
4071
5098
|
inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
|
|
4072
5099
|
},
|
|
5100
|
+
{
|
|
5101
|
+
name: "maintain_memory",
|
|
5102
|
+
description: "Compact old completed memory and prune stale/noisy indexes to keep long-lived large projects fast. Defaults to dry_run=true; automatic safe maintenance also runs periodically.",
|
|
5103
|
+
inputSchema: toJsonSchemaCompat(MaintainMemoryArgsSchema),
|
|
5104
|
+
},
|
|
4073
5105
|
{
|
|
4074
5106
|
name: "prune_index",
|
|
4075
5107
|
description: "Prune noisy auto-indexed items (code_chunk/doc_chunk + symbols). Useful after tightening ignore rules to shrink the index and improve search relevance.",
|
|
@@ -4257,6 +5289,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4257
5289
|
],
|
|
4258
5290
|
};
|
|
4259
5291
|
}
|
|
5292
|
+
if (toolName === "maintain_memory") {
|
|
5293
|
+
const args = MaintainMemoryArgsSchema.parse(rawArgs);
|
|
5294
|
+
flushPendingChangeBuffer();
|
|
5295
|
+
const result = runMemoryMaintenance(args, "manual");
|
|
5296
|
+
return {
|
|
5297
|
+
content: [
|
|
5298
|
+
{
|
|
5299
|
+
type: "text",
|
|
5300
|
+
text: toolCompactOrJson("maintain_memory", result, compactMaintenanceText(result), args.format),
|
|
5301
|
+
},
|
|
5302
|
+
],
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
4260
5305
|
if (toolName === "sync_change_intent") {
|
|
4261
5306
|
const args = SyncChangeIntentArgsSchema.parse(rawArgs);
|
|
4262
5307
|
flushPendingChangeBuffer();
|
|
@@ -4373,6 +5418,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4373
5418
|
const notesLimit = args.notes_limit;
|
|
4374
5419
|
const conventionsLimit = args.conventions_limit;
|
|
4375
5420
|
const decisionsLimit = args.decisions_limit;
|
|
5421
|
+
const currentContextLimit = args.current_context_limit;
|
|
4376
5422
|
const recent = listRecentRequirementsStmt.all(requirementsLimit);
|
|
4377
5423
|
const items = recent.map((req) => {
|
|
4378
5424
|
const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
|
|
@@ -4388,6 +5434,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4388
5434
|
const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
|
|
4389
5435
|
const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
|
|
4390
5436
|
const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
|
|
5437
|
+
const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
|
|
4391
5438
|
const pending_offset = args.pending_offset;
|
|
4392
5439
|
const pending_limit = args.pending_limit;
|
|
4393
5440
|
const pendingDbRows = listPendingChangesStmt.all();
|
|
@@ -4396,12 +5443,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4396
5443
|
const pending_truncated = mergedPending.truncated;
|
|
4397
5444
|
const pending_changes = mergedPending.page;
|
|
4398
5445
|
const q = args.query?.trim() ?? "";
|
|
5446
|
+
const semanticKinds = args.kinds?.length ? args.kinds : BOOTSTRAP_DEFAULT_CONTEXT_KINDS;
|
|
4399
5447
|
const semantic = q
|
|
4400
5448
|
? await Promise.race([
|
|
4401
5449
|
semanticSearchHybridInternal({
|
|
4402
5450
|
query: q,
|
|
4403
5451
|
topK: args.top_k,
|
|
4404
|
-
kinds:
|
|
5452
|
+
kinds: semanticKinds,
|
|
4405
5453
|
includeContent,
|
|
4406
5454
|
previewChars,
|
|
4407
5455
|
contentMaxChars,
|
|
@@ -4418,6 +5466,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4418
5466
|
pending_returned: pending_changes.length,
|
|
4419
5467
|
requirements_returned: items.length,
|
|
4420
5468
|
decisions_returned: decisions.length,
|
|
5469
|
+
current_context_returned: current_context.length,
|
|
4421
5470
|
conventions_returned: conventions.length,
|
|
4422
5471
|
semantic_mode: semantic?.mode ?? null,
|
|
4423
5472
|
semantic_matches: semantic?.matches?.length ?? 0,
|
|
@@ -4444,11 +5493,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4444
5493
|
changes_limit: changesLimit,
|
|
4445
5494
|
notes_limit: notesLimit,
|
|
4446
5495
|
decisions_limit: decisionsLimit,
|
|
5496
|
+
current_context_limit: currentContextLimit,
|
|
4447
5497
|
conventions_limit: conventionsLimit,
|
|
4448
5498
|
},
|
|
4449
5499
|
project_summary,
|
|
4450
5500
|
decisions,
|
|
4451
5501
|
conventions,
|
|
5502
|
+
current_context,
|
|
4452
5503
|
recent_notes,
|
|
4453
5504
|
pending_total,
|
|
4454
5505
|
pending_offset,
|
|
@@ -4478,6 +5529,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4478
5529
|
const notesLimit = args.notes_limit;
|
|
4479
5530
|
const conventionsLimit = args.conventions_limit;
|
|
4480
5531
|
const decisionsLimit = args.decisions_limit;
|
|
5532
|
+
const currentContextLimit = args.current_context_limit;
|
|
4481
5533
|
const recent = listRecentRequirementsStmt.all(requirementsLimit);
|
|
4482
5534
|
const items = recent.map((req) => {
|
|
4483
5535
|
const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
|
|
@@ -4493,6 +5545,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4493
5545
|
const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
|
|
4494
5546
|
const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
|
|
4495
5547
|
const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
|
|
5548
|
+
const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
|
|
4496
5549
|
const pending_offset = args.pending_offset;
|
|
4497
5550
|
const pending_limit = args.pending_limit;
|
|
4498
5551
|
const pendingDbRows = listPendingChangesStmt.all();
|
|
@@ -4506,6 +5559,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4506
5559
|
requirements_returned: items.length,
|
|
4507
5560
|
notes_returned: recent_notes.length,
|
|
4508
5561
|
decisions_returned: decisions.length,
|
|
5562
|
+
current_context_returned: current_context.length,
|
|
4509
5563
|
conventions_returned: conventions.length,
|
|
4510
5564
|
});
|
|
4511
5565
|
const outputValue = {
|
|
@@ -4530,11 +5584,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4530
5584
|
changes_limit: changesLimit,
|
|
4531
5585
|
notes_limit: notesLimit,
|
|
4532
5586
|
decisions_limit: decisionsLimit,
|
|
5587
|
+
current_context_limit: currentContextLimit,
|
|
4533
5588
|
conventions_limit: conventionsLimit,
|
|
4534
5589
|
},
|
|
4535
5590
|
project_summary,
|
|
4536
5591
|
decisions,
|
|
4537
5592
|
conventions,
|
|
5593
|
+
current_context,
|
|
4538
5594
|
recent_notes,
|
|
4539
5595
|
pending_total,
|
|
4540
5596
|
pending_offset,
|