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