@memstack/core 0.7.2 → 0.8.0
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 +75 -4
- package/dist/index.cjs +665 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -5
- package/dist/index.d.ts +178 -5
- package/dist/index.js +660 -117
- package/dist/index.js.map +1 -1
- package/package.json +21 -4
package/dist/index.cjs
CHANGED
|
@@ -726,6 +726,7 @@ var MemoryStore = class {
|
|
|
726
726
|
|
|
727
727
|
// src/adapters/storage/memory.ts
|
|
728
728
|
var InMemoryStorageAdapter = class {
|
|
729
|
+
capabilities = { multiProcess: false };
|
|
729
730
|
memories = /* @__PURE__ */ new Map();
|
|
730
731
|
async initialize() {
|
|
731
732
|
}
|
|
@@ -821,8 +822,10 @@ var InMemoryStorageAdapter = class {
|
|
|
821
822
|
results.sort((a, b) => b.importance - a.importance || b._touchedAt.getTime() - a._touchedAt.getTime());
|
|
822
823
|
break;
|
|
823
824
|
}
|
|
824
|
-
|
|
825
|
-
r
|
|
825
|
+
if (query.touch !== false) {
|
|
826
|
+
for (const r of results.slice(0, query.limit ?? 10)) {
|
|
827
|
+
r._touchedAt = /* @__PURE__ */ new Date();
|
|
828
|
+
}
|
|
826
829
|
}
|
|
827
830
|
const limit = query.limit ?? 10;
|
|
828
831
|
return results.slice(0, limit);
|
|
@@ -854,6 +857,25 @@ var InMemoryStorageAdapter = class {
|
|
|
854
857
|
}
|
|
855
858
|
};
|
|
856
859
|
|
|
860
|
+
// src/enrichment.ts
|
|
861
|
+
var ENRICHMENT_MAX_TOKENS = 1024;
|
|
862
|
+
function extractJson(text) {
|
|
863
|
+
let cleaned = text.trim();
|
|
864
|
+
const fenceMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
865
|
+
if (fenceMatch) cleaned = fenceMatch[1].trim();
|
|
866
|
+
return cleaned;
|
|
867
|
+
}
|
|
868
|
+
function parseTags(text, max = 5) {
|
|
869
|
+
try {
|
|
870
|
+
const parsed = JSON.parse(extractJson(text));
|
|
871
|
+
if (Array.isArray(parsed) && parsed.every((t) => typeof t === "string")) {
|
|
872
|
+
return parsed.map((t) => t.toLowerCase().trim()).slice(0, max);
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
return text.split(",").map((t) => t.replace(/[\[\]"]/g, "").trim().toLowerCase()).filter((t) => t.length > 0).slice(0, max);
|
|
877
|
+
}
|
|
878
|
+
|
|
857
879
|
// src/client.ts
|
|
858
880
|
var MemStack = class {
|
|
859
881
|
memory;
|
|
@@ -886,7 +908,7 @@ var MemStack = class {
|
|
|
886
908
|
const result = await this.config.llm.complete({
|
|
887
909
|
system: 'Analyze the memory and respond with a JSON object: {"importance": <0.0-1.0>, "tags": [<array of 1-5 lowercase single-word tags>]}. Return ONLY the JSON, nothing else.',
|
|
888
910
|
user: input.content,
|
|
889
|
-
maxTokens:
|
|
911
|
+
maxTokens: ENRICHMENT_MAX_TOKENS,
|
|
890
912
|
temperature: 0
|
|
891
913
|
});
|
|
892
914
|
const parsed = this._parseEnrichmentJson(result.text);
|
|
@@ -896,7 +918,7 @@ var MemStack = class {
|
|
|
896
918
|
const result = await this.config.llm.complete({
|
|
897
919
|
system: "Rate the importance of the following memory for an AI agent on a scale of 0.0 to 1.0, where 0.0 is trivial and 1.0 is critical. Return ONLY a single float number, nothing else.",
|
|
898
920
|
user: input.content,
|
|
899
|
-
maxTokens:
|
|
921
|
+
maxTokens: ENRICHMENT_MAX_TOKENS,
|
|
900
922
|
temperature: 0
|
|
901
923
|
});
|
|
902
924
|
const parsed = parseFloat(result.text.trim());
|
|
@@ -905,10 +927,10 @@ var MemStack = class {
|
|
|
905
927
|
const result = await this.config.llm.complete({
|
|
906
928
|
system: 'Extract 1-5 concise, lowercase, single-word tags from the following memory. Return ONLY a JSON array of strings, nothing else. Example: ["combat", "goblin", "forest"]',
|
|
907
929
|
user: input.content,
|
|
908
|
-
maxTokens:
|
|
930
|
+
maxTokens: ENRICHMENT_MAX_TOKENS,
|
|
909
931
|
temperature: 0
|
|
910
932
|
});
|
|
911
|
-
tags =
|
|
933
|
+
tags = parseTags(result.text);
|
|
912
934
|
}
|
|
913
935
|
} catch (err) {
|
|
914
936
|
this.config.hooks?.onError?.(
|
|
@@ -1026,7 +1048,7 @@ var MemStack = class {
|
|
|
1026
1048
|
}
|
|
1027
1049
|
_parseEnrichmentJson(text) {
|
|
1028
1050
|
try {
|
|
1029
|
-
const json = JSON.parse(
|
|
1051
|
+
const json = JSON.parse(extractJson(text));
|
|
1030
1052
|
const result = {};
|
|
1031
1053
|
if (typeof json.importance === "number" && Number.isFinite(json.importance)) {
|
|
1032
1054
|
result.importance = Math.max(0, Math.min(1, json.importance));
|
|
@@ -1039,25 +1061,448 @@ var MemStack = class {
|
|
|
1039
1061
|
return {};
|
|
1040
1062
|
}
|
|
1041
1063
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// src/retrieval/text.ts
|
|
1067
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1068
|
+
"a",
|
|
1069
|
+
"an",
|
|
1070
|
+
"and",
|
|
1071
|
+
"are",
|
|
1072
|
+
"as",
|
|
1073
|
+
"at",
|
|
1074
|
+
"be",
|
|
1075
|
+
"by",
|
|
1076
|
+
"did",
|
|
1077
|
+
"do",
|
|
1078
|
+
"does",
|
|
1079
|
+
"for",
|
|
1080
|
+
"from",
|
|
1081
|
+
"how",
|
|
1082
|
+
"i",
|
|
1083
|
+
"in",
|
|
1084
|
+
"is",
|
|
1085
|
+
"it",
|
|
1086
|
+
"its",
|
|
1087
|
+
"me",
|
|
1088
|
+
"my",
|
|
1089
|
+
"of",
|
|
1090
|
+
"on",
|
|
1091
|
+
"or",
|
|
1092
|
+
"our",
|
|
1093
|
+
"that",
|
|
1094
|
+
"the",
|
|
1095
|
+
"this",
|
|
1096
|
+
"to",
|
|
1097
|
+
"was",
|
|
1098
|
+
"we",
|
|
1099
|
+
"were",
|
|
1100
|
+
"what",
|
|
1101
|
+
"when",
|
|
1102
|
+
"where",
|
|
1103
|
+
"which",
|
|
1104
|
+
"who",
|
|
1105
|
+
"why",
|
|
1106
|
+
"with",
|
|
1107
|
+
"you",
|
|
1108
|
+
"your",
|
|
1109
|
+
// Verbs that frame a question ("what do we use?") without saying what it is about.
|
|
1110
|
+
"can",
|
|
1111
|
+
"could",
|
|
1112
|
+
"has",
|
|
1113
|
+
"have",
|
|
1114
|
+
"should",
|
|
1115
|
+
"use",
|
|
1116
|
+
"used",
|
|
1117
|
+
"uses",
|
|
1118
|
+
"using",
|
|
1119
|
+
"would"
|
|
1120
|
+
]);
|
|
1121
|
+
function tokenize(text) {
|
|
1122
|
+
return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
1123
|
+
}
|
|
1124
|
+
function queryTerms(text) {
|
|
1125
|
+
const words = [...new Set(tokenize(text))];
|
|
1126
|
+
const meaningful = words.filter((w) => !STOPWORDS.has(w));
|
|
1127
|
+
return [...new Set((meaningful.length > 0 ? meaningful : words).map(stem))];
|
|
1128
|
+
}
|
|
1129
|
+
function documentTerms(text) {
|
|
1130
|
+
return tokenize(text).map(stem);
|
|
1131
|
+
}
|
|
1132
|
+
var STEP2 = {
|
|
1133
|
+
ational: "ate",
|
|
1134
|
+
tional: "tion",
|
|
1135
|
+
enci: "ence",
|
|
1136
|
+
anci: "ance",
|
|
1137
|
+
izer: "ize",
|
|
1138
|
+
bli: "ble",
|
|
1139
|
+
alli: "al",
|
|
1140
|
+
entli: "ent",
|
|
1141
|
+
eli: "e",
|
|
1142
|
+
ousli: "ous",
|
|
1143
|
+
ization: "ize",
|
|
1144
|
+
ation: "ate",
|
|
1145
|
+
ator: "ate",
|
|
1146
|
+
alism: "al",
|
|
1147
|
+
iveness: "ive",
|
|
1148
|
+
fulness: "ful",
|
|
1149
|
+
ousness: "ous",
|
|
1150
|
+
aliti: "al",
|
|
1151
|
+
iviti: "ive",
|
|
1152
|
+
biliti: "ble",
|
|
1153
|
+
logi: "log"
|
|
1154
|
+
};
|
|
1155
|
+
var STEP3 = {
|
|
1156
|
+
icate: "ic",
|
|
1157
|
+
ative: "",
|
|
1158
|
+
alize: "al",
|
|
1159
|
+
iciti: "ic",
|
|
1160
|
+
ical: "ic",
|
|
1161
|
+
ful: "",
|
|
1162
|
+
ness: ""
|
|
1163
|
+
};
|
|
1164
|
+
var STEP4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
|
|
1165
|
+
var C = "[^aeiou]";
|
|
1166
|
+
var V = "[aeiouy]";
|
|
1167
|
+
var CS = `${C}[^aeiouy]*`;
|
|
1168
|
+
var VS = `${V}[aeiou]*`;
|
|
1169
|
+
var MGR0 = new RegExp(`^(${CS})?${VS}${CS}`);
|
|
1170
|
+
var MEQ1 = new RegExp(`^(${CS})?${VS}${CS}(${VS})?$`);
|
|
1171
|
+
var MGR1 = new RegExp(`^(${CS})?${VS}${CS}${VS}${CS}`);
|
|
1172
|
+
var HAS_VOWEL = new RegExp(`^(${CS})?${V}`);
|
|
1173
|
+
var CVC = new RegExp(`^${CS}${V}[^aeiouwxy]$`);
|
|
1174
|
+
function stem(word) {
|
|
1175
|
+
if (word.length < 3 || !/^[a-z]+$/.test(word)) return word;
|
|
1176
|
+
let w = word;
|
|
1177
|
+
const firstY = w[0] === "y";
|
|
1178
|
+
if (firstY) w = "Y" + w.slice(1);
|
|
1179
|
+
if (/(ss|i)es$/.test(w)) w = w.slice(0, -2);
|
|
1180
|
+
else if (/[^s]s$/.test(w)) w = w.slice(0, -1);
|
|
1181
|
+
let m;
|
|
1182
|
+
if (m = w.match(/^(.+?)eed$/)) {
|
|
1183
|
+
if (MGR0.test(m[1])) w = w.slice(0, -1);
|
|
1184
|
+
} else if (m = w.match(/^(.+?)(ed|ing)$/)) {
|
|
1185
|
+
const base = m[1];
|
|
1186
|
+
if (HAS_VOWEL.test(base)) {
|
|
1187
|
+
w = base;
|
|
1188
|
+
if (/(at|bl|iz)$/.test(w)) w += "e";
|
|
1189
|
+
else if (/([^aeiouylsz])\1$/.test(w)) w = w.slice(0, -1);
|
|
1190
|
+
else if (CVC.test(w)) w += "e";
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if ((m = w.match(/^(.+?)y$/)) && HAS_VOWEL.test(m[1])) w = m[1] + "i";
|
|
1194
|
+
if (m = w.match(/^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/)) {
|
|
1195
|
+
if (MGR0.test(m[1])) w = m[1] + STEP2[m[2]];
|
|
1196
|
+
}
|
|
1197
|
+
if (m = w.match(/^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/)) {
|
|
1198
|
+
if (MGR0.test(m[1])) w = m[1] + STEP3[m[2]];
|
|
1199
|
+
}
|
|
1200
|
+
if (m = w.match(STEP4)) {
|
|
1201
|
+
if (MGR1.test(m[1])) w = m[1];
|
|
1202
|
+
} else if (m = w.match(/^(.+?)(s|t)(ion)$/)) {
|
|
1203
|
+
const base = m[1] + m[2];
|
|
1204
|
+
if (MGR1.test(base)) w = base;
|
|
1205
|
+
}
|
|
1206
|
+
if (m = w.match(/^(.+?)e$/)) {
|
|
1207
|
+
const base = m[1];
|
|
1208
|
+
if (MGR1.test(base) || MEQ1.test(base) && !CVC.test(base)) w = base;
|
|
1209
|
+
}
|
|
1210
|
+
if (/ll$/.test(w) && MGR1.test(w)) w = w.slice(0, -1);
|
|
1211
|
+
if (firstY) w = "y" + w.slice(1);
|
|
1212
|
+
return w;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/retrieval/LexicalRetriever.ts
|
|
1216
|
+
var K1 = 1.2;
|
|
1217
|
+
var B = 0.75;
|
|
1218
|
+
var PREFIX_WEIGHT = 0.5;
|
|
1219
|
+
var MIN_PREFIX_LENGTH = 3;
|
|
1220
|
+
var LexicalRetriever = class {
|
|
1221
|
+
constructor(storage, config = {}) {
|
|
1222
|
+
this.storage = storage;
|
|
1223
|
+
this.candidateLimit = config.candidateLimit ?? 2e3;
|
|
1224
|
+
this.touch = config.touch ?? true;
|
|
1225
|
+
}
|
|
1226
|
+
storage;
|
|
1227
|
+
candidateLimit;
|
|
1228
|
+
touch;
|
|
1229
|
+
async recall(query) {
|
|
1230
|
+
const limit = query.limit ?? 10;
|
|
1231
|
+
const maxChars = query.maxChars ?? 8e3;
|
|
1232
|
+
const terms = queryTerms(query.query ?? "");
|
|
1233
|
+
let ranked = [];
|
|
1234
|
+
let candidates;
|
|
1235
|
+
if (terms.length > 0 && this.storage.search && this.storage.capabilities?.textSearch) {
|
|
1236
|
+
ranked = (await this.storage.search({
|
|
1237
|
+
actorIds: query.actorIds,
|
|
1238
|
+
query: query.query,
|
|
1239
|
+
memoryTypes: query.memoryTypes,
|
|
1240
|
+
limit: this.candidateLimit
|
|
1241
|
+
})).map(({ memory, score }) => ({ memory, score }));
|
|
1242
|
+
} else if (terms.length > 0) {
|
|
1243
|
+
candidates = await this.loadCandidates(query);
|
|
1244
|
+
ranked = rank(candidates, terms);
|
|
1245
|
+
}
|
|
1246
|
+
const fallback = ranked.length === 0;
|
|
1247
|
+
if (fallback) {
|
|
1248
|
+
candidates ??= await this.loadCandidates(query);
|
|
1249
|
+
ranked = [...candidates].sort(byImportanceThenRecency).map((memory) => ({ memory, score: 0 }));
|
|
1250
|
+
}
|
|
1251
|
+
const hits = withinBudget(ranked, limit, maxChars);
|
|
1252
|
+
if (this.touch) await this.touchAll(hits);
|
|
1253
|
+
return { hits, fallback };
|
|
1254
|
+
}
|
|
1255
|
+
async loadCandidates(query) {
|
|
1256
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1257
|
+
for (const actorId of new Set(query.actorIds)) {
|
|
1258
|
+
const memories = await this.storage.retrieve({
|
|
1259
|
+
actorId,
|
|
1260
|
+
memoryTypes: query.memoryTypes,
|
|
1261
|
+
limit: this.candidateLimit,
|
|
1262
|
+
strategy: "important",
|
|
1263
|
+
touch: false
|
|
1264
|
+
});
|
|
1265
|
+
for (const memory of memories) byId.set(memory.id, memory);
|
|
1266
|
+
}
|
|
1267
|
+
return [...byId.values()];
|
|
1268
|
+
}
|
|
1269
|
+
async touchAll(hits) {
|
|
1270
|
+
if (!this.storage.touch) return;
|
|
1271
|
+
await Promise.all(
|
|
1272
|
+
hits.map(
|
|
1273
|
+
({ memory }) => this.storage.touch(memory.id).catch(() => {
|
|
1274
|
+
})
|
|
1275
|
+
)
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
function rank(memories, terms) {
|
|
1280
|
+
if (memories.length === 0) return [];
|
|
1281
|
+
const candidates = memories.map((memory) => {
|
|
1282
|
+
const tokens = documentTerms(`${memory.content} ${memory.tags.join(" ")}`);
|
|
1283
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1284
|
+
for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
1285
|
+
return { memory, terms: counts, length: tokens.length };
|
|
1286
|
+
});
|
|
1287
|
+
const avgLength = candidates.reduce((sum, c) => sum + c.length, 0) / candidates.length || 1;
|
|
1288
|
+
const n = candidates.length;
|
|
1289
|
+
const frequencies = terms.map(
|
|
1290
|
+
(term) => candidates.map((c) => {
|
|
1291
|
+
let tf = c.terms.get(term) ?? 0;
|
|
1292
|
+
if (term.length >= MIN_PREFIX_LENGTH) {
|
|
1293
|
+
for (const [docTerm, count] of c.terms) {
|
|
1294
|
+
if (docTerm !== term && docTerm.startsWith(term)) tf += count * PREFIX_WEIGHT;
|
|
1295
|
+
}
|
|
1047
1296
|
}
|
|
1048
|
-
|
|
1297
|
+
return tf;
|
|
1298
|
+
})
|
|
1299
|
+
);
|
|
1300
|
+
const hits = [];
|
|
1301
|
+
candidates.forEach((c, i) => {
|
|
1302
|
+
let score = 0;
|
|
1303
|
+
frequencies.forEach((perCandidate) => {
|
|
1304
|
+
const tf = perCandidate[i];
|
|
1305
|
+
if (tf === 0) return;
|
|
1306
|
+
const df = perCandidate.filter((f) => f > 0).length;
|
|
1307
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
1308
|
+
score += idf * (tf * (K1 + 1) / (tf + K1 * (1 - B + B * c.length / avgLength)));
|
|
1309
|
+
});
|
|
1310
|
+
if (score > 0) hits.push({ memory: c.memory, score });
|
|
1311
|
+
});
|
|
1312
|
+
return hits.sort((a, b) => b.score - a.score || byImportanceThenRecency(a.memory, b.memory));
|
|
1313
|
+
}
|
|
1314
|
+
function byImportanceThenRecency(a, b) {
|
|
1315
|
+
return b.importance - a.importance || b.createdAt.getTime() - a.createdAt.getTime();
|
|
1316
|
+
}
|
|
1317
|
+
function withinBudget(ranked, limit, maxChars) {
|
|
1318
|
+
const hits = [];
|
|
1319
|
+
let chars = 0;
|
|
1320
|
+
for (const hit of ranked) {
|
|
1321
|
+
if (hits.length >= limit) break;
|
|
1322
|
+
const size = hit.memory.content.length;
|
|
1323
|
+
if (hits.length > 0 && chars + size > maxChars) continue;
|
|
1324
|
+
hits.push(hit);
|
|
1325
|
+
chars += size;
|
|
1326
|
+
}
|
|
1327
|
+
return hits;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/harness/namespaces.ts
|
|
1331
|
+
var GLOBAL_NAMESPACE = "global";
|
|
1332
|
+
function projectNamespace(projectId) {
|
|
1333
|
+
if (!projectId) throw new Error("projectId is required");
|
|
1334
|
+
return `project:${projectId}`;
|
|
1335
|
+
}
|
|
1336
|
+
function sessionNamespace(projectId, sessionId) {
|
|
1337
|
+
if (!projectId || !sessionId) throw new Error("projectId and sessionId are required");
|
|
1338
|
+
return `session:${projectId}:${sessionId}`;
|
|
1339
|
+
}
|
|
1340
|
+
function defaultRecallNamespaces(projectId) {
|
|
1341
|
+
return [projectNamespace(projectId), GLOBAL_NAMESPACE];
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/harness/HarnessMemory.ts
|
|
1345
|
+
var MAX_TAGS = 8;
|
|
1346
|
+
var TAG_PROMPT = 'You tag memories so they can be found later by keyword search. Return 3-5 lowercase, single-word topic tags as a JSON array of strings, and nothing else. Include broader category words a person might search for, not only words already in the text. Example: for "This project uses Hono" return ["framework", "backend", "web", "server", "hono"].';
|
|
1347
|
+
var HarnessMemory = class {
|
|
1348
|
+
storage;
|
|
1349
|
+
llm;
|
|
1350
|
+
retriever;
|
|
1351
|
+
autoTags;
|
|
1352
|
+
tagTimeoutMs;
|
|
1353
|
+
maxContentChars;
|
|
1354
|
+
onError;
|
|
1355
|
+
initialized;
|
|
1356
|
+
constructor(config) {
|
|
1357
|
+
this.storage = config.storage;
|
|
1358
|
+
this.llm = config.llm;
|
|
1359
|
+
this.retriever = new LexicalRetriever(config.storage, { candidateLimit: config.candidateLimit });
|
|
1360
|
+
this.autoTags = config.autoTags ?? true;
|
|
1361
|
+
this.tagTimeoutMs = config.tagTimeoutMs ?? 1e4;
|
|
1362
|
+
this.maxContentChars = config.maxContentChars ?? 8e3;
|
|
1363
|
+
this.onError = config.onError;
|
|
1364
|
+
}
|
|
1365
|
+
async remember(input) {
|
|
1366
|
+
const content = input.content.trim();
|
|
1367
|
+
if (!content) throw validationError("Memory content is empty");
|
|
1368
|
+
if (content.length > this.maxContentChars) {
|
|
1369
|
+
throw validationError(`Memory content is longer than ${this.maxContentChars} characters`, {
|
|
1370
|
+
length: content.length
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
if (!input.namespace) throw validationError("Namespace is required");
|
|
1374
|
+
if (input.importance !== void 0 && !(input.importance >= 0 && input.importance <= 1)) {
|
|
1375
|
+
throw validationError("Importance must be between 0 and 1", { importance: input.importance });
|
|
1376
|
+
}
|
|
1377
|
+
await this.ensureInit();
|
|
1378
|
+
const llmTags = this.autoTags ? await this.suggestTags(content) : [];
|
|
1379
|
+
const tags = [...new Set([...input.tags ?? [], ...llmTags].map((t) => t.toLowerCase().trim()).filter(Boolean))];
|
|
1380
|
+
return this.storage.store({
|
|
1381
|
+
actorId: input.namespace,
|
|
1382
|
+
content,
|
|
1383
|
+
memoryType: input.kind ?? "observation",
|
|
1384
|
+
importance: input.importance ?? 0.5,
|
|
1385
|
+
tags: tags.slice(0, MAX_TAGS),
|
|
1386
|
+
metadata: { source: input.source }
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
async recall(input) {
|
|
1390
|
+
await this.ensureInit();
|
|
1391
|
+
return this.retriever.recall({
|
|
1392
|
+
actorIds: input.namespaces,
|
|
1393
|
+
query: input.query,
|
|
1394
|
+
memoryTypes: input.kinds,
|
|
1395
|
+
limit: input.limit,
|
|
1396
|
+
maxChars: input.maxChars
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
/** The memory with this ID, or null when it does not exist or is outside `namespaces`. */
|
|
1400
|
+
async get(id, namespaces) {
|
|
1401
|
+
await this.ensureInit();
|
|
1402
|
+
const memory = await this.storage.get(id);
|
|
1403
|
+
return memory && namespaces.includes(memory.actorId) ? memory : null;
|
|
1404
|
+
}
|
|
1405
|
+
/** Delete a memory. Memories outside `namespaces` are reported as not found. */
|
|
1406
|
+
async forget(id, namespaces) {
|
|
1407
|
+
const memory = await this.get(id, namespaces);
|
|
1408
|
+
if (!memory) throw notFound("Memory", id);
|
|
1409
|
+
await this.storage.delete(id);
|
|
1410
|
+
}
|
|
1411
|
+
/** Number of memories in each namespace. */
|
|
1412
|
+
async stats(namespaces) {
|
|
1413
|
+
await this.ensureInit();
|
|
1414
|
+
const counts = {};
|
|
1415
|
+
for (const namespace of new Set(namespaces)) {
|
|
1416
|
+
counts[namespace] = await this.storage.count({ actorId: namespace });
|
|
1417
|
+
}
|
|
1418
|
+
return counts;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Moves every memory from one namespace to another, keeping content,
|
|
1422
|
+
* kind, importance, tags, provenance, and creation time. Memories get new
|
|
1423
|
+
* IDs. Used when a project's ID changes, e.g. after its first commit.
|
|
1424
|
+
* Safe to run from two processes at once: a copy whose original was
|
|
1425
|
+
* already moved by the other process is removed again.
|
|
1426
|
+
*/
|
|
1427
|
+
async moveNamespace(from, to) {
|
|
1428
|
+
if (!from || !to) throw validationError("Both namespaces are required");
|
|
1429
|
+
if (from === to) return 0;
|
|
1430
|
+
await this.ensureInit();
|
|
1431
|
+
let moved = 0;
|
|
1432
|
+
for (; ; ) {
|
|
1433
|
+
const batch = await this.storage.retrieve({ actorId: from, limit: 500, strategy: "recent", touch: false });
|
|
1434
|
+
if (batch.length === 0) return moved;
|
|
1435
|
+
let progressed = false;
|
|
1436
|
+
for (const memory of batch) {
|
|
1437
|
+
const copy = await this.storage.store({
|
|
1438
|
+
actorId: to,
|
|
1439
|
+
content: memory.content,
|
|
1440
|
+
memoryType: memory.memoryType,
|
|
1441
|
+
importance: memory.importance,
|
|
1442
|
+
emotionalValence: memory.emotionalValence,
|
|
1443
|
+
tags: memory.tags,
|
|
1444
|
+
metadata: memory.metadata,
|
|
1445
|
+
createdAt: memory.createdAt,
|
|
1446
|
+
expiresAt: memory.expiresAt
|
|
1447
|
+
});
|
|
1448
|
+
try {
|
|
1449
|
+
await this.storage.delete(memory.id);
|
|
1450
|
+
moved++;
|
|
1451
|
+
progressed = true;
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
await this.storage.delete(copy.id);
|
|
1454
|
+
if (!(error instanceof MemStackError && error.code === "NOT_FOUND")) throw error;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!progressed) return moved;
|
|
1049
1458
|
}
|
|
1050
|
-
return text.split(",").map((t) => t.replace(/[\[\]"]/g, "").trim().toLowerCase()).filter((t) => t.length > 0).slice(0, 5);
|
|
1051
1459
|
}
|
|
1052
|
-
/**
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1460
|
+
/**
|
|
1461
|
+
* Adopts memories a project stored under IDs it had before, such as
|
|
1462
|
+
* before its first commit. Costs one count per previous ID when there is
|
|
1463
|
+
* nothing to move. Returns the number of memories moved.
|
|
1464
|
+
*/
|
|
1465
|
+
async adoptProjects(previousIds, projectId) {
|
|
1466
|
+
const target = projectNamespace(projectId);
|
|
1467
|
+
let moved = 0;
|
|
1468
|
+
for (const id of new Set(previousIds)) {
|
|
1469
|
+
const from = projectNamespace(id);
|
|
1470
|
+
if (from === target) continue;
|
|
1471
|
+
await this.ensureInit();
|
|
1472
|
+
if (await this.storage.count({ actorId: from }) === 0) continue;
|
|
1473
|
+
moved += await this.moveNamespace(from, target);
|
|
1474
|
+
}
|
|
1475
|
+
return moved;
|
|
1476
|
+
}
|
|
1477
|
+
ensureInit() {
|
|
1478
|
+
this.initialized ??= this.storage.initialize().catch((error) => {
|
|
1479
|
+
this.initialized = void 0;
|
|
1480
|
+
throw error;
|
|
1481
|
+
});
|
|
1482
|
+
return this.initialized;
|
|
1483
|
+
}
|
|
1484
|
+
async suggestTags(content) {
|
|
1485
|
+
let timer;
|
|
1486
|
+
try {
|
|
1487
|
+
const timeout = new Promise((_, reject) => {
|
|
1488
|
+
timer = setTimeout(() => reject(new Error(`Tagging timed out after ${this.tagTimeoutMs} ms`)), this.tagTimeoutMs);
|
|
1489
|
+
});
|
|
1490
|
+
const result = await Promise.race([
|
|
1491
|
+
this.llm.complete({ system: TAG_PROMPT, user: content, maxTokens: ENRICHMENT_MAX_TOKENS, temperature: 0 }),
|
|
1492
|
+
timeout
|
|
1493
|
+
]);
|
|
1494
|
+
if (!result.text.trim()) throw new Error("Tagging returned an empty reply");
|
|
1495
|
+
return parseTags(result.text).filter((t) => /^[\p{L}\p{N}-]+$/u.test(t));
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)), "harness-tagging");
|
|
1498
|
+
return [];
|
|
1499
|
+
} finally {
|
|
1500
|
+
clearTimeout(timer);
|
|
1501
|
+
}
|
|
1058
1502
|
}
|
|
1059
1503
|
};
|
|
1060
1504
|
var DiskStorageAdapter = class {
|
|
1505
|
+
capabilities = { multiProcess: false };
|
|
1061
1506
|
dir;
|
|
1062
1507
|
_writeLocks = /* @__PURE__ */ new Map();
|
|
1063
1508
|
_idIndex = /* @__PURE__ */ new Map();
|
|
@@ -1248,26 +1693,28 @@ var DiskStorageAdapter = class {
|
|
|
1248
1693
|
);
|
|
1249
1694
|
break;
|
|
1250
1695
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
r
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1696
|
+
if (query.touch !== false) {
|
|
1697
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
1698
|
+
const toTouch = results.slice(0, query.limit ?? 10);
|
|
1699
|
+
const touchedActors = /* @__PURE__ */ new Set();
|
|
1700
|
+
for (const r of toTouch) {
|
|
1701
|
+
r._touchedAt = nowStr;
|
|
1702
|
+
touchedActors.add(r.actorId);
|
|
1703
|
+
}
|
|
1704
|
+
for (const actorId of touchedActors) {
|
|
1705
|
+
await this._withWriteLock(actorId, async () => {
|
|
1706
|
+
const records = await this._readFile(actorId);
|
|
1707
|
+
let changed = false;
|
|
1708
|
+
for (const r of records) {
|
|
1709
|
+
const touched = toTouch.find((t) => t.id === r.id);
|
|
1710
|
+
if (touched) {
|
|
1711
|
+
r._touchedAt = nowStr;
|
|
1712
|
+
changed = true;
|
|
1713
|
+
}
|
|
1267
1714
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
}
|
|
1715
|
+
if (changed) await this._writeFile(actorId, records);
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1271
1718
|
}
|
|
1272
1719
|
const limit = query.limit ?? 10;
|
|
1273
1720
|
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
@@ -1418,6 +1865,7 @@ var DiskStorageAdapter = class {
|
|
|
1418
1865
|
}
|
|
1419
1866
|
};
|
|
1420
1867
|
var MarkdownStorageAdapter = class {
|
|
1868
|
+
capabilities = { multiProcess: false };
|
|
1421
1869
|
dir;
|
|
1422
1870
|
oneFilePerActor;
|
|
1423
1871
|
_cache = null;
|
|
@@ -1934,6 +2382,7 @@ var HybridStorageAdapter = class {
|
|
|
1934
2382
|
|
|
1935
2383
|
// src/adapters/storage/postgres.ts
|
|
1936
2384
|
var PostgresStorageAdapter = class {
|
|
2385
|
+
capabilities = { multiProcess: true };
|
|
1937
2386
|
pool;
|
|
1938
2387
|
table;
|
|
1939
2388
|
vectorDimensions;
|
|
@@ -1969,7 +2418,8 @@ var PostgresStorageAdapter = class {
|
|
|
1969
2418
|
const connStr = this._connectionString ?? this._buildConnString();
|
|
1970
2419
|
try {
|
|
1971
2420
|
const { default: createPostgres } = await import('postgres');
|
|
1972
|
-
const options = {
|
|
2421
|
+
const options = { onnotice: () => {
|
|
2422
|
+
} };
|
|
1973
2423
|
if (this._ssl) {
|
|
1974
2424
|
options.ssl = typeof this._ssl === "object" ? this._ssl : "require";
|
|
1975
2425
|
}
|
|
@@ -2002,35 +2452,50 @@ var PostgresStorageAdapter = class {
|
|
|
2002
2452
|
}
|
|
2003
2453
|
return connStr;
|
|
2004
2454
|
}
|
|
2455
|
+
// One DO block, so it runs in a single transaction on one connection: the
|
|
2456
|
+
// advisory lock makes processes that start together (Claude Code and Codex)
|
|
2457
|
+
// migrate one at a time instead of racing on CREATE ... IF NOT EXISTS,
|
|
2458
|
+
// which Postgres reports as a duplicate key in pg_type.
|
|
2005
2459
|
async _runMigration() {
|
|
2006
|
-
try {
|
|
2007
|
-
await this.pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
2008
|
-
} catch {
|
|
2009
|
-
}
|
|
2010
2460
|
await this.pool.query(`
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2461
|
+
DO $memstack$
|
|
2462
|
+
BEGIN
|
|
2463
|
+
PERFORM pg_advisory_xact_lock(hashtext('memstack:migrate:${this.table}'));
|
|
2464
|
+
BEGIN
|
|
2465
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
2466
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2467
|
+
NULL; -- pgvector may not be available
|
|
2468
|
+
END;
|
|
2469
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
2470
|
+
id TEXT PRIMARY KEY,
|
|
2471
|
+
actor_id TEXT NOT NULL,
|
|
2472
|
+
memory_type TEXT NOT NULL DEFAULT 'interaction',
|
|
2473
|
+
content TEXT NOT NULL,
|
|
2474
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
2475
|
+
emotional_valence REAL NOT NULL DEFAULT 0,
|
|
2476
|
+
tags JSONB NOT NULL DEFAULT '[]',
|
|
2477
|
+
embedding vector(${this.vectorDimensions}),
|
|
2478
|
+
source_id TEXT,
|
|
2479
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
2480
|
+
expires_at TIMESTAMPTZ,
|
|
2481
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
2482
|
+
touched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
2483
|
+
);
|
|
2484
|
+
BEGIN
|
|
2485
|
+
CREATE INDEX IF NOT EXISTS idx_${this.table}_actor ON ${this.table} (actor_id);
|
|
2486
|
+
CREATE INDEX IF NOT EXISTS idx_${this.table}_created ON ${this.table} (created_at DESC);
|
|
2487
|
+
CREATE INDEX IF NOT EXISTS idx_${this.table}_importance ON ${this.table} (importance DESC);
|
|
2488
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2489
|
+
NULL; -- index creation may fail; non-critical
|
|
2490
|
+
END;
|
|
2491
|
+
BEGIN
|
|
2492
|
+
CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding ON ${this.table} USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
|
|
2493
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2494
|
+
NULL; -- index creation may fail; non-critical
|
|
2495
|
+
END;
|
|
2496
|
+
END
|
|
2497
|
+
$memstack$
|
|
2026
2498
|
`);
|
|
2027
|
-
try {
|
|
2028
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_actor ON ${this.table} (actor_id)`);
|
|
2029
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_created ON ${this.table} (created_at DESC)`);
|
|
2030
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_importance ON ${this.table} (importance DESC)`);
|
|
2031
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding ON ${this.table} USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64)`);
|
|
2032
|
-
} catch {
|
|
2033
|
-
}
|
|
2034
2499
|
}
|
|
2035
2500
|
generateId() {
|
|
2036
2501
|
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -2165,9 +2630,11 @@ var PostgresStorageAdapter = class {
|
|
|
2165
2630
|
const sql = `SELECT * FROM ${this.table} ${where} ORDER BY ${orderBy} LIMIT $${paramIdx}`;
|
|
2166
2631
|
params.push(limit);
|
|
2167
2632
|
const { rows } = await this.pool.query(sql, params);
|
|
2168
|
-
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2633
|
+
if (query.touch !== false) {
|
|
2634
|
+
for (const row of rows.slice(0, limit)) {
|
|
2635
|
+
const r = row;
|
|
2636
|
+
await this.pool.query(`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`, [r.id]);
|
|
2637
|
+
}
|
|
2171
2638
|
}
|
|
2172
2639
|
return rows.map((r) => this._rowToMemory(r));
|
|
2173
2640
|
}
|
|
@@ -2231,9 +2698,11 @@ var PostgresStorageAdapter = class {
|
|
|
2231
2698
|
params.push(embedding);
|
|
2232
2699
|
params.push(limit);
|
|
2233
2700
|
const { rows } = await this.pool.query(sql, params);
|
|
2234
|
-
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2701
|
+
if (query.touch !== false) {
|
|
2702
|
+
for (const row of rows.slice(0, limit)) {
|
|
2703
|
+
const r = row;
|
|
2704
|
+
await this.pool.query(`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`, [r.id]);
|
|
2705
|
+
}
|
|
2237
2706
|
}
|
|
2238
2707
|
return rows.map((r) => this._rowToMemory(r));
|
|
2239
2708
|
}
|
|
@@ -2257,6 +2726,7 @@ var PostgresStorageAdapter = class {
|
|
|
2257
2726
|
|
|
2258
2727
|
// src/adapters/storage/redis.ts
|
|
2259
2728
|
var RedisStorageAdapter = class {
|
|
2729
|
+
capabilities = { multiProcess: true };
|
|
2260
2730
|
redis;
|
|
2261
2731
|
prefix;
|
|
2262
2732
|
constructor(config) {
|
|
@@ -2394,11 +2864,13 @@ var RedisStorageAdapter = class {
|
|
|
2394
2864
|
);
|
|
2395
2865
|
break;
|
|
2396
2866
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
r
|
|
2401
|
-
|
|
2867
|
+
if (query.touch !== false) {
|
|
2868
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
2869
|
+
const toTouch = results.slice(0, query.limit ?? 10);
|
|
2870
|
+
for (const r of toTouch) {
|
|
2871
|
+
r._touchedAt = nowStr;
|
|
2872
|
+
await this.redis.set(this._memKey(r.id), JSON.stringify(r));
|
|
2873
|
+
}
|
|
2402
2874
|
}
|
|
2403
2875
|
const limit = query.limit ?? 10;
|
|
2404
2876
|
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
@@ -3332,6 +3804,7 @@ var LanceDBStorageAdapter = class {
|
|
|
3332
3804
|
|
|
3333
3805
|
// src/adapters/storage/mongodb.ts
|
|
3334
3806
|
var MongoDBStorageAdapter = class {
|
|
3807
|
+
capabilities = { multiProcess: true };
|
|
3335
3808
|
collection;
|
|
3336
3809
|
vectorDimensions;
|
|
3337
3810
|
constructor(config) {
|
|
@@ -3380,7 +3853,16 @@ var MongoDBStorageAdapter = class {
|
|
|
3380
3853
|
expiresAt: input.expiresAt,
|
|
3381
3854
|
createdAt: input.createdAt ?? now
|
|
3382
3855
|
};
|
|
3383
|
-
|
|
3856
|
+
if (input.id) {
|
|
3857
|
+
const { _id, actorId, memoryType, createdAt, ...fields } = this.toDoc(memory);
|
|
3858
|
+
await this.collection.updateOne(
|
|
3859
|
+
{ _id },
|
|
3860
|
+
{ $set: fields, $setOnInsert: { actorId, memoryType, createdAt } },
|
|
3861
|
+
{ upsert: true }
|
|
3862
|
+
);
|
|
3863
|
+
} else {
|
|
3864
|
+
await this.collection.insertOne(this.toDoc(memory));
|
|
3865
|
+
}
|
|
3384
3866
|
return memory;
|
|
3385
3867
|
}
|
|
3386
3868
|
async storeBatch(inputs) {
|
|
@@ -3510,6 +3992,33 @@ var MongoDBStorageAdapter = class {
|
|
|
3510
3992
|
};
|
|
3511
3993
|
|
|
3512
3994
|
// src/adapters/storage/sqlite.ts
|
|
3995
|
+
var MIGRATIONS_TABLE = "memstack_schema_migrations";
|
|
3996
|
+
var MIGRATIONS = [
|
|
3997
|
+
{
|
|
3998
|
+
version: 1,
|
|
3999
|
+
sql: (t) => `
|
|
4000
|
+
CREATE TABLE IF NOT EXISTS ${t} (
|
|
4001
|
+
id TEXT PRIMARY KEY,
|
|
4002
|
+
actor_id TEXT NOT NULL,
|
|
4003
|
+
memory_type TEXT NOT NULL DEFAULT 'interaction',
|
|
4004
|
+
content TEXT NOT NULL,
|
|
4005
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
4006
|
+
emotional_valence REAL NOT NULL DEFAULT 0,
|
|
4007
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
4008
|
+
embedding TEXT,
|
|
4009
|
+
source_id TEXT,
|
|
4010
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
4011
|
+
expires_at TEXT,
|
|
4012
|
+
created_at TEXT NOT NULL,
|
|
4013
|
+
touched_at TEXT NOT NULL
|
|
4014
|
+
);
|
|
4015
|
+
CREATE INDEX IF NOT EXISTS idx_${t}_actor ON ${t}(actor_id);
|
|
4016
|
+
CREATE INDEX IF NOT EXISTS idx_${t}_created ON ${t}(created_at DESC);
|
|
4017
|
+
CREATE INDEX IF NOT EXISTS idx_${t}_importance ON ${t}(importance DESC);
|
|
4018
|
+
`
|
|
4019
|
+
}
|
|
4020
|
+
];
|
|
4021
|
+
MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
3513
4022
|
function cosineSimilarity(a, b) {
|
|
3514
4023
|
if (a.length === 0 || b.length === 0) return 0;
|
|
3515
4024
|
const dims = Math.min(a.length, b.length);
|
|
@@ -3525,9 +4034,12 @@ function cosineSimilarity(a, b) {
|
|
|
3525
4034
|
return mag === 0 ? 0 : dot / mag;
|
|
3526
4035
|
}
|
|
3527
4036
|
var SQLiteStorageAdapter = class {
|
|
4037
|
+
capabilities = { multiProcess: true };
|
|
3528
4038
|
db;
|
|
3529
4039
|
table;
|
|
3530
4040
|
vectorDimensions;
|
|
4041
|
+
busyTimeoutMs;
|
|
4042
|
+
walMode;
|
|
3531
4043
|
constructor(config) {
|
|
3532
4044
|
if (!config.db) {
|
|
3533
4045
|
throw configError("SQLiteStorageAdapter requires a better-sqlite3 database instance");
|
|
@@ -3538,33 +4050,67 @@ var SQLiteStorageAdapter = class {
|
|
|
3538
4050
|
throw configError(`Invalid table name: "${this.table}". Use only alphanumeric characters and underscores.`);
|
|
3539
4051
|
}
|
|
3540
4052
|
this.vectorDimensions = config.vectorDimensions ?? 1536;
|
|
4053
|
+
this.busyTimeoutMs = config.busyTimeoutMs ?? 5e3;
|
|
4054
|
+
if (!Number.isInteger(this.busyTimeoutMs) || this.busyTimeoutMs < 0) {
|
|
4055
|
+
throw configError(`Invalid busyTimeoutMs: ${config.busyTimeoutMs}. Use a non-negative integer.`);
|
|
4056
|
+
}
|
|
4057
|
+
this.walMode = config.walMode ?? true;
|
|
3541
4058
|
}
|
|
3542
4059
|
async initialize() {
|
|
3543
|
-
this.db.exec(`
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
)
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
4060
|
+
this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
|
|
4061
|
+
if (this.walMode) this.db.exec("PRAGMA journal_mode = WAL");
|
|
4062
|
+
this.migrate();
|
|
4063
|
+
}
|
|
4064
|
+
/** Schema version applied to this adapter's table, 0 if none. */
|
|
4065
|
+
schemaVersion() {
|
|
4066
|
+
const row = this.db.prepare(
|
|
4067
|
+
`SELECT version FROM ${MIGRATIONS_TABLE} WHERE table_name = ?`
|
|
4068
|
+
).get(this.table);
|
|
4069
|
+
return row?.version ?? 0;
|
|
4070
|
+
}
|
|
4071
|
+
// Versions are tracked per table rather than with PRAGMA user_version,
|
|
4072
|
+
// so adapters with different table names can share one database file.
|
|
4073
|
+
migrate() {
|
|
4074
|
+
for (const migration of MIGRATIONS) {
|
|
4075
|
+
this.transaction(() => {
|
|
4076
|
+
this.db.exec(
|
|
4077
|
+
`CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (table_name TEXT PRIMARY KEY, version INTEGER NOT NULL)`
|
|
4078
|
+
);
|
|
4079
|
+
if (this.schemaVersion() >= migration.version) return;
|
|
4080
|
+
this.db.exec(migration.sql(this.table));
|
|
4081
|
+
this.db.prepare(
|
|
4082
|
+
`INSERT INTO ${MIGRATIONS_TABLE} (table_name, version) VALUES (?, ?)
|
|
4083
|
+
ON CONFLICT(table_name) DO UPDATE SET version = excluded.version`
|
|
4084
|
+
).run(this.table, migration.version);
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
// BEGIN IMMEDIATE takes the write lock up front, so a concurrent writer
|
|
4089
|
+
// waits on busy_timeout instead of failing when it upgrades a read lock.
|
|
4090
|
+
transaction(fn) {
|
|
4091
|
+
if (this.db.inTransaction) return fn();
|
|
4092
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
4093
|
+
try {
|
|
4094
|
+
const result = fn();
|
|
4095
|
+
this.db.exec("COMMIT");
|
|
4096
|
+
return result;
|
|
4097
|
+
} catch (error) {
|
|
4098
|
+
if (this.db.inTransaction !== false) {
|
|
4099
|
+
try {
|
|
4100
|
+
this.db.exec("ROLLBACK");
|
|
4101
|
+
} catch {
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
throw error;
|
|
4105
|
+
}
|
|
3563
4106
|
}
|
|
3564
4107
|
generateId() {
|
|
3565
4108
|
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
3566
4109
|
}
|
|
3567
4110
|
async store(input) {
|
|
4111
|
+
return this.insert(input);
|
|
4112
|
+
}
|
|
4113
|
+
insert(input) {
|
|
3568
4114
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3569
4115
|
const id = input.id ?? this.generateId();
|
|
3570
4116
|
this.db.prepare(
|
|
@@ -3611,11 +4157,7 @@ var SQLiteStorageAdapter = class {
|
|
|
3611
4157
|
};
|
|
3612
4158
|
}
|
|
3613
4159
|
async storeBatch(inputs) {
|
|
3614
|
-
|
|
3615
|
-
for (const input of inputs) {
|
|
3616
|
-
results.push(await this.store(input));
|
|
3617
|
-
}
|
|
3618
|
-
return results;
|
|
4160
|
+
return this.transaction(() => inputs.map((input) => this.insert(input)));
|
|
3619
4161
|
}
|
|
3620
4162
|
async get(id) {
|
|
3621
4163
|
const row = this.db.prepare(
|
|
@@ -3661,7 +4203,6 @@ var SQLiteStorageAdapter = class {
|
|
|
3661
4203
|
conditions.push("created_at <= ?");
|
|
3662
4204
|
params.push(query.createdBefore.toISOString());
|
|
3663
4205
|
}
|
|
3664
|
-
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
3665
4206
|
const hasSemantic = (query.strategy === "semantic" || query.strategy === "hybrid") && embedding && embedding.length > 0;
|
|
3666
4207
|
let orderBy;
|
|
3667
4208
|
switch (query.strategy) {
|
|
@@ -3682,9 +4223,8 @@ var SQLiteStorageAdapter = class {
|
|
|
3682
4223
|
break;
|
|
3683
4224
|
}
|
|
3684
4225
|
const limit = query.limit ?? 10;
|
|
3685
|
-
const sql = `SELECT * FROM ${this.table} ${
|
|
3686
|
-
|
|
3687
|
-
let results = rows;
|
|
4226
|
+
const sql = `SELECT * FROM ${this.table} WHERE ${conditions.join(" AND ")} ORDER BY ${orderBy}`;
|
|
4227
|
+
let results = this.db.prepare(sql).all(...params);
|
|
3688
4228
|
if (query.query) {
|
|
3689
4229
|
const q = query.query.toLowerCase();
|
|
3690
4230
|
results = results.filter((r) => r.content.toLowerCase().includes(q));
|
|
@@ -3704,17 +4244,12 @@ var SQLiteStorageAdapter = class {
|
|
|
3704
4244
|
if (query.strategy === "hybrid") {
|
|
3705
4245
|
scored.sort((a, b) => b.score + b.row.importance - (a.score + a.row.importance));
|
|
3706
4246
|
}
|
|
3707
|
-
const topK = scored.slice(0, limit);
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
}
|
|
3711
|
-
return topK.map(({ row }) => this._rowToMemory(row));
|
|
4247
|
+
const topK = scored.slice(0, limit).map(({ row }) => row);
|
|
4248
|
+
this.touchRows(topK, query.touch);
|
|
4249
|
+
return topK.map((row) => this._rowToMemory(row));
|
|
3712
4250
|
}
|
|
3713
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3714
4251
|
const limited = results.slice(0, limit);
|
|
3715
|
-
|
|
3716
|
-
this.db.prepare(`UPDATE ${this.table} SET touched_at = ? WHERE id = ?`).run(now, row.id);
|
|
3717
|
-
}
|
|
4252
|
+
this.touchRows(limited, query.touch);
|
|
3718
4253
|
return limited.map((r) => this._rowToMemory(r));
|
|
3719
4254
|
}
|
|
3720
4255
|
async count(filter) {
|
|
@@ -3749,6 +4284,14 @@ var SQLiteStorageAdapter = class {
|
|
|
3749
4284
|
async close() {
|
|
3750
4285
|
this.db.close();
|
|
3751
4286
|
}
|
|
4287
|
+
touchRows(rows, touch) {
|
|
4288
|
+
if (touch === false || rows.length === 0) return;
|
|
4289
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4290
|
+
const stmt = this.db.prepare(`UPDATE ${this.table} SET touched_at = ? WHERE id = ?`);
|
|
4291
|
+
this.transaction(() => {
|
|
4292
|
+
for (const row of rows) stmt.run(now, row.id);
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
3752
4295
|
_rowToMemory(row) {
|
|
3753
4296
|
return {
|
|
3754
4297
|
id: row.id,
|
|
@@ -4257,10 +4800,13 @@ var GroqLLMAdapter = class {
|
|
|
4257
4800
|
exports.AnthropicLLMAdapter = AnthropicLLMAdapter;
|
|
4258
4801
|
exports.CohereEmbeddingAdapter = CohereEmbeddingAdapter;
|
|
4259
4802
|
exports.DiskStorageAdapter = DiskStorageAdapter;
|
|
4803
|
+
exports.GLOBAL_NAMESPACE = GLOBAL_NAMESPACE;
|
|
4260
4804
|
exports.GroqLLMAdapter = GroqLLMAdapter;
|
|
4805
|
+
exports.HarnessMemory = HarnessMemory;
|
|
4261
4806
|
exports.HybridStorageAdapter = HybridStorageAdapter;
|
|
4262
4807
|
exports.InMemoryStorageAdapter = InMemoryStorageAdapter;
|
|
4263
4808
|
exports.LanceDBStorageAdapter = LanceDBStorageAdapter;
|
|
4809
|
+
exports.LexicalRetriever = LexicalRetriever;
|
|
4264
4810
|
exports.MarkdownStorageAdapter = MarkdownStorageAdapter;
|
|
4265
4811
|
exports.MemStack = MemStack;
|
|
4266
4812
|
exports.MemStackError = MemStackError;
|
|
@@ -4275,7 +4821,10 @@ exports.RedisStorageAdapter = RedisStorageAdapter;
|
|
|
4275
4821
|
exports.SQLiteStorageAdapter = SQLiteStorageAdapter;
|
|
4276
4822
|
exports.WeaviateStorageAdapter = WeaviateStorageAdapter;
|
|
4277
4823
|
exports.configError = configError;
|
|
4824
|
+
exports.defaultRecallNamespaces = defaultRecallNamespaces;
|
|
4278
4825
|
exports.notFound = notFound;
|
|
4826
|
+
exports.projectNamespace = projectNamespace;
|
|
4827
|
+
exports.sessionNamespace = sessionNamespace;
|
|
4279
4828
|
exports.storageError = storageError;
|
|
4280
4829
|
exports.validationError = validationError;
|
|
4281
4830
|
//# sourceMappingURL=index.cjs.map
|