@smyslenny/agent-memory 2.2.0 → 3.1.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/CHANGELOG.md +45 -41
- package/README.en.md +153 -0
- package/README.md +86 -153
- package/dist/bin/agent-memory.js +28 -534
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +50 -167
- package/dist/index.js +289 -692
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +584 -748
- package/dist/mcp/server.js.map +1 -1
- package/docs/design/0014-memory-core-dedup.md +722 -0
- package/docs/design/TEMPLATE.md +67 -0
- package/package.json +2 -3
- package/README.zh-CN.md +0 -170
package/dist/index.js
CHANGED
|
@@ -565,108 +565,6 @@ function deletePath(db, id) {
|
|
|
565
565
|
return result.changes > 0;
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
// src/core/link.ts
|
|
569
|
-
function createLink(db, sourceId, targetId, relation, weight = 1, agent_id) {
|
|
570
|
-
const sourceAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id;
|
|
571
|
-
const targetAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(targetId)?.agent_id;
|
|
572
|
-
if (!sourceAgent) throw new Error(`Source memory not found: ${sourceId}`);
|
|
573
|
-
if (!targetAgent) throw new Error(`Target memory not found: ${targetId}`);
|
|
574
|
-
if (sourceAgent !== targetAgent) throw new Error("Cross-agent links are not allowed");
|
|
575
|
-
if (agent_id && agent_id !== sourceAgent) throw new Error("Agent mismatch for link");
|
|
576
|
-
const agentId = agent_id ?? sourceAgent;
|
|
577
|
-
db.prepare(
|
|
578
|
-
`INSERT OR REPLACE INTO links (agent_id, source_id, target_id, relation, weight, created_at)
|
|
579
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
580
|
-
).run(agentId, sourceId, targetId, relation, weight, now());
|
|
581
|
-
return { agent_id: agentId, source_id: sourceId, target_id: targetId, relation, weight, created_at: now() };
|
|
582
|
-
}
|
|
583
|
-
function getLinks(db, memoryId, agent_id) {
|
|
584
|
-
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id ?? "default";
|
|
585
|
-
return db.prepare("SELECT * FROM links WHERE agent_id = ? AND (source_id = ? OR target_id = ?)").all(agentId, memoryId, memoryId);
|
|
586
|
-
}
|
|
587
|
-
function getOutgoingLinks(db, sourceId, agent_id) {
|
|
588
|
-
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id ?? "default";
|
|
589
|
-
return db.prepare("SELECT * FROM links WHERE agent_id = ? AND source_id = ?").all(agentId, sourceId);
|
|
590
|
-
}
|
|
591
|
-
function traverse(db, startId, maxHops = 2, agent_id) {
|
|
592
|
-
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(startId)?.agent_id ?? "default";
|
|
593
|
-
const visited = /* @__PURE__ */ new Set();
|
|
594
|
-
const results = [];
|
|
595
|
-
const queue = [
|
|
596
|
-
{ id: startId, hop: 0, relation: "self" }
|
|
597
|
-
];
|
|
598
|
-
while (queue.length > 0) {
|
|
599
|
-
const current = queue.shift();
|
|
600
|
-
if (visited.has(current.id)) continue;
|
|
601
|
-
visited.add(current.id);
|
|
602
|
-
if (current.hop > 0) {
|
|
603
|
-
results.push(current);
|
|
604
|
-
}
|
|
605
|
-
if (current.hop < maxHops) {
|
|
606
|
-
const links = db.prepare("SELECT target_id, relation FROM links WHERE agent_id = ? AND source_id = ?").all(agentId, current.id);
|
|
607
|
-
for (const link of links) {
|
|
608
|
-
if (!visited.has(link.target_id)) {
|
|
609
|
-
queue.push({
|
|
610
|
-
id: link.target_id,
|
|
611
|
-
hop: current.hop + 1,
|
|
612
|
-
relation: link.relation
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const reverseLinks = db.prepare("SELECT source_id, relation FROM links WHERE agent_id = ? AND target_id = ?").all(agentId, current.id);
|
|
617
|
-
for (const link of reverseLinks) {
|
|
618
|
-
if (!visited.has(link.source_id)) {
|
|
619
|
-
queue.push({
|
|
620
|
-
id: link.source_id,
|
|
621
|
-
hop: current.hop + 1,
|
|
622
|
-
relation: link.relation
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return results;
|
|
629
|
-
}
|
|
630
|
-
function deleteLink(db, sourceId, targetId, agent_id) {
|
|
631
|
-
const agentId = agent_id ?? db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(sourceId)?.agent_id ?? "default";
|
|
632
|
-
const result = db.prepare("DELETE FROM links WHERE agent_id = ? AND source_id = ? AND target_id = ?").run(agentId, sourceId, targetId);
|
|
633
|
-
return result.changes > 0;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// src/core/snapshot.ts
|
|
637
|
-
function createSnapshot(db, memoryId, action, changedBy) {
|
|
638
|
-
const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
|
|
639
|
-
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
640
|
-
const id = newId();
|
|
641
|
-
db.prepare(
|
|
642
|
-
`INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
|
|
643
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
644
|
-
).run(id, memoryId, memory.content, changedBy ?? null, action, now());
|
|
645
|
-
return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
|
|
646
|
-
}
|
|
647
|
-
function getSnapshots(db, memoryId) {
|
|
648
|
-
return db.prepare("SELECT * FROM snapshots WHERE memory_id = ? ORDER BY created_at DESC").all(memoryId);
|
|
649
|
-
}
|
|
650
|
-
function getSnapshot(db, id) {
|
|
651
|
-
return db.prepare("SELECT * FROM snapshots WHERE id = ?").get(id) ?? null;
|
|
652
|
-
}
|
|
653
|
-
function rollback(db, snapshotId) {
|
|
654
|
-
const snapshot = getSnapshot(db, snapshotId);
|
|
655
|
-
if (!snapshot) return false;
|
|
656
|
-
createSnapshot(db, snapshot.memory_id, "update", "rollback");
|
|
657
|
-
db.prepare("UPDATE memories SET content = ?, updated_at = ? WHERE id = ?").run(
|
|
658
|
-
snapshot.content,
|
|
659
|
-
now(),
|
|
660
|
-
snapshot.memory_id
|
|
661
|
-
);
|
|
662
|
-
db.prepare("DELETE FROM memories_fts WHERE id = ?").run(snapshot.memory_id);
|
|
663
|
-
db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(
|
|
664
|
-
snapshot.memory_id,
|
|
665
|
-
tokenizeForIndex(snapshot.content)
|
|
666
|
-
);
|
|
667
|
-
return true;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
568
|
// src/core/guard.ts
|
|
671
569
|
function guard(db, input) {
|
|
672
570
|
const hash = contentHash(input.content);
|
|
@@ -880,503 +778,314 @@ function buildFtsQuery(text) {
|
|
|
880
778
|
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
881
779
|
}
|
|
882
780
|
|
|
883
|
-
// src/
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
/(多少|几个|哪个|哪些|哪里)/,
|
|
894
|
-
// Chinese - lookup patterns
|
|
895
|
-
/(查一下|找一下|看看|搜一下)/,
|
|
896
|
-
/(.+)是什么$/
|
|
897
|
-
],
|
|
898
|
-
temporal: [
|
|
899
|
-
// English
|
|
900
|
-
/^(when|what time|how long)\b/i,
|
|
901
|
-
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
902
|
-
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
903
|
-
// Chinese - time expressions
|
|
904
|
-
/什么时候/,
|
|
905
|
-
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
906
|
-
/(几月|几号|几点|多久|多长时间)/,
|
|
907
|
-
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
908
|
-
// Date patterns
|
|
909
|
-
/\d{4}[-/.]\d{1,2}/,
|
|
910
|
-
/\d{1,2}月\d{1,2}[日号]/,
|
|
911
|
-
// Chinese - temporal context
|
|
912
|
-
/(历史|记录|日志|以来|至今|期间)/
|
|
913
|
-
],
|
|
914
|
-
causal: [
|
|
915
|
-
// English
|
|
916
|
-
/^(why|how come|what caused)\b/i,
|
|
917
|
-
/\b(because|due to|reason|cause|result)\b/i,
|
|
918
|
-
// Chinese - causal questions
|
|
919
|
-
/为(什么|啥|何)/,
|
|
920
|
-
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
921
|
-
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
922
|
-
/(为啥|凭啥|凭什么)/,
|
|
923
|
-
// Chinese - problem/diagnosis
|
|
924
|
-
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
925
|
-
],
|
|
926
|
-
exploratory: [
|
|
927
|
-
// English
|
|
928
|
-
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
929
|
-
/^(what do you think|what about|any)\b/i,
|
|
930
|
-
/\b(overview|summary|list|compare)\b/i,
|
|
931
|
-
// Chinese - exploratory
|
|
932
|
-
/(怎么样|怎样|如何)/,
|
|
933
|
-
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
934
|
-
/(有哪些|有什么|有没有)/,
|
|
935
|
-
/(关于|对于|至于|关联)/,
|
|
936
|
-
/(总结|概括|梳理|回顾|盘点)/,
|
|
937
|
-
// Chinese - opinion/analysis
|
|
938
|
-
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
939
|
-
/(对比|比较|区别|差异|优缺点)/
|
|
940
|
-
]
|
|
941
|
-
};
|
|
942
|
-
var CN_STRUCTURE_BOOSTS = {
|
|
943
|
-
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
944
|
-
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
945
|
-
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
946
|
-
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
947
|
-
};
|
|
948
|
-
function classifyIntent(query) {
|
|
949
|
-
const scores = {
|
|
950
|
-
factual: 0,
|
|
951
|
-
exploratory: 0,
|
|
952
|
-
temporal: 0,
|
|
953
|
-
causal: 0
|
|
781
|
+
// src/sleep/sync.ts
|
|
782
|
+
function syncOne(db, input) {
|
|
783
|
+
const memInput = {
|
|
784
|
+
content: input.content,
|
|
785
|
+
type: input.type ?? "event",
|
|
786
|
+
priority: input.priority,
|
|
787
|
+
emotion_val: input.emotion_val,
|
|
788
|
+
source: input.source,
|
|
789
|
+
agent_id: input.agent_id,
|
|
790
|
+
uri: input.uri
|
|
954
791
|
};
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
792
|
+
const guardResult = guard(db, memInput);
|
|
793
|
+
switch (guardResult.action) {
|
|
794
|
+
case "skip":
|
|
795
|
+
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
796
|
+
case "add": {
|
|
797
|
+
const mem = createMemory(db, memInput);
|
|
798
|
+
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
799
|
+
if (input.uri) {
|
|
800
|
+
try {
|
|
801
|
+
createPath(db, mem.id, input.uri);
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
959
804
|
}
|
|
805
|
+
return { action: "added", memoryId: mem.id, reason: guardResult.reason };
|
|
960
806
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
scores[intent] += 0.5;
|
|
966
|
-
}
|
|
807
|
+
case "update": {
|
|
808
|
+
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
809
|
+
updateMemory(db, guardResult.existingId, { content: input.content });
|
|
810
|
+
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
967
811
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
let maxIntent = "factual";
|
|
975
|
-
let maxScore = 0;
|
|
976
|
-
for (const [intent, score] of Object.entries(scores)) {
|
|
977
|
-
if (score > maxScore) {
|
|
978
|
-
maxScore = score;
|
|
979
|
-
maxIntent = intent;
|
|
812
|
+
case "merge": {
|
|
813
|
+
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
814
|
+
return { action: "skipped", reason: "Missing merge data" };
|
|
815
|
+
}
|
|
816
|
+
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
817
|
+
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
980
818
|
}
|
|
981
819
|
}
|
|
982
|
-
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
983
|
-
const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
|
|
984
|
-
return { intent: maxIntent, confidence };
|
|
985
|
-
}
|
|
986
|
-
function getStrategy(intent) {
|
|
987
|
-
switch (intent) {
|
|
988
|
-
case "factual":
|
|
989
|
-
return { boostRecent: false, boostPriority: true, limit: 5 };
|
|
990
|
-
case "temporal":
|
|
991
|
-
return { boostRecent: true, boostPriority: false, limit: 10 };
|
|
992
|
-
case "causal":
|
|
993
|
-
return { boostRecent: false, boostPriority: false, limit: 10 };
|
|
994
|
-
case "exploratory":
|
|
995
|
-
return { boostRecent: false, boostPriority: false, limit: 15 };
|
|
996
|
-
}
|
|
997
820
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
try {
|
|
1004
|
-
const apiResults = await provider.rerank(query, documents);
|
|
1005
|
-
const scoreMap = new Map(apiResults.map((r) => [r.index, r.relevance_score]));
|
|
1006
|
-
return results.map((r, i) => {
|
|
1007
|
-
const score = scoreMap.get(i);
|
|
1008
|
-
if (score === void 0) return r;
|
|
1009
|
-
return {
|
|
1010
|
-
...r,
|
|
1011
|
-
score,
|
|
1012
|
-
matchReason: `${r.matchReason}+rerank`
|
|
1013
|
-
};
|
|
1014
|
-
});
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
console.warn("[agent-memory] External rerank failed, falling back:", err);
|
|
1017
|
-
return results;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
function rerank(results, opts) {
|
|
1021
|
-
const now2 = Date.now();
|
|
1022
|
-
const scored = results.map((r) => {
|
|
1023
|
-
let finalScore = r.score;
|
|
1024
|
-
if (opts.boostPriority) {
|
|
1025
|
-
const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
|
|
1026
|
-
finalScore *= priorityMultiplier;
|
|
1027
|
-
}
|
|
1028
|
-
if (opts.boostRecent && r.memory.updated_at) {
|
|
1029
|
-
const age = now2 - new Date(r.memory.updated_at).getTime();
|
|
1030
|
-
const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
|
|
1031
|
-
const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
|
|
1032
|
-
finalScore *= recencyBoost;
|
|
821
|
+
function syncBatch(db, inputs) {
|
|
822
|
+
const results = [];
|
|
823
|
+
const transaction = db.transaction(() => {
|
|
824
|
+
for (const input of inputs) {
|
|
825
|
+
results.push(syncOne(db, input));
|
|
1033
826
|
}
|
|
1034
|
-
finalScore *= Math.max(0.1, r.memory.vitality);
|
|
1035
|
-
return { ...r, score: finalScore };
|
|
1036
827
|
});
|
|
1037
|
-
|
|
1038
|
-
return
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// src/search/embeddings.ts
|
|
1042
|
-
function encodeEmbedding(vector) {
|
|
1043
|
-
const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
1044
|
-
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
1045
|
-
}
|
|
1046
|
-
function decodeEmbedding(buf) {
|
|
1047
|
-
const copy = Buffer.from(buf);
|
|
1048
|
-
return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
|
|
1049
|
-
}
|
|
1050
|
-
function upsertEmbedding(db, input) {
|
|
1051
|
-
const ts = now();
|
|
1052
|
-
const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
|
|
1053
|
-
const blob = encodeEmbedding(vec);
|
|
1054
|
-
db.prepare(
|
|
1055
|
-
`INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
|
|
1056
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1057
|
-
ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
|
|
1058
|
-
dim = excluded.dim,
|
|
1059
|
-
vector = excluded.vector,
|
|
1060
|
-
updated_at = excluded.updated_at`
|
|
1061
|
-
).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
|
|
1062
|
-
}
|
|
1063
|
-
function getEmbedding(db, agent_id, memory_id, model) {
|
|
1064
|
-
const row = db.prepare(
|
|
1065
|
-
"SELECT agent_id, memory_id, model, dim, vector, created_at, updated_at FROM embeddings WHERE agent_id = ? AND memory_id = ? AND model = ?"
|
|
1066
|
-
).get(agent_id, memory_id, model);
|
|
1067
|
-
if (!row) return null;
|
|
1068
|
-
return { ...row, vector: decodeEmbedding(row.vector) };
|
|
1069
|
-
}
|
|
1070
|
-
function listEmbeddings(db, agent_id, model) {
|
|
1071
|
-
const rows = db.prepare(
|
|
1072
|
-
"SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
|
|
1073
|
-
).all(agent_id, model);
|
|
1074
|
-
return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
|
|
828
|
+
transaction();
|
|
829
|
+
return results;
|
|
1075
830
|
}
|
|
1076
831
|
|
|
1077
|
-
// src/
|
|
1078
|
-
function
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
832
|
+
// src/ingest/ingest.ts
|
|
833
|
+
function slugify(input) {
|
|
834
|
+
return input.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff\s-]/g, " ").trim().replace(/\s+/g, "-").slice(0, 64) || "item";
|
|
835
|
+
}
|
|
836
|
+
function classifyIngestType(text) {
|
|
837
|
+
const lower = text.toLowerCase();
|
|
838
|
+
if (/##\s*身份|\bidentity\b|\b我是\b|我是/.test(text)) {
|
|
839
|
+
return "identity";
|
|
840
|
+
}
|
|
841
|
+
if (/##\s*情感|❤️|💕|爱你|感动|难过|开心|害怕|想念|表白/.test(text)) {
|
|
842
|
+
return "emotion";
|
|
843
|
+
}
|
|
844
|
+
if (/##\s*决策|##\s*技术|选型|教训|\bknowledge\b|⚠️|复盘|经验/.test(text)) {
|
|
845
|
+
return "knowledge";
|
|
846
|
+
}
|
|
847
|
+
if (/\d{4}-\d{2}-\d{2}|发生了|完成了|今天|昨日|刚刚|部署|上线/.test(text)) {
|
|
848
|
+
return "event";
|
|
849
|
+
}
|
|
850
|
+
if (lower.length <= 12) return "event";
|
|
851
|
+
return "knowledge";
|
|
852
|
+
}
|
|
853
|
+
function splitIngestBlocks(text) {
|
|
854
|
+
const headingRegex = /^##\s+(.+)$/gm;
|
|
855
|
+
const matches = [...text.matchAll(headingRegex)];
|
|
856
|
+
const blocks = [];
|
|
857
|
+
if (matches.length > 0) {
|
|
858
|
+
for (let i = 0; i < matches.length; i++) {
|
|
859
|
+
const match = matches[i];
|
|
860
|
+
const start = match.index ?? 0;
|
|
861
|
+
const end = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
|
|
862
|
+
const raw = text.slice(start, end).trim();
|
|
863
|
+
const lines = raw.split("\n");
|
|
864
|
+
const title = lines[0].replace(/^##\s+/, "").trim();
|
|
865
|
+
const content = lines.slice(1).join("\n").trim();
|
|
866
|
+
if (content) blocks.push({ title, content });
|
|
1109
867
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
return
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
868
|
+
return blocks;
|
|
869
|
+
}
|
|
870
|
+
const bullets = text.split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter(Boolean);
|
|
871
|
+
if (bullets.length > 0) {
|
|
872
|
+
return bullets.map((content, i) => ({ title: `bullet-${i + 1}`, content }));
|
|
873
|
+
}
|
|
874
|
+
const plain = text.trim();
|
|
875
|
+
if (!plain) return [];
|
|
876
|
+
return [{ title: "ingest", content: plain }];
|
|
877
|
+
}
|
|
878
|
+
function extractIngestItems(text, source) {
|
|
879
|
+
const blocks = splitIngestBlocks(text);
|
|
880
|
+
return blocks.map((block, index) => {
|
|
881
|
+
const merged = `${block.title}
|
|
882
|
+
${block.content}`;
|
|
883
|
+
const type = classifyIngestType(merged);
|
|
884
|
+
const domain = type === "identity" ? "core" : type;
|
|
885
|
+
const sourcePart = slugify(source ?? "ingest");
|
|
886
|
+
const uri = `${domain}://ingest/${sourcePart}/${index + 1}-${slugify(block.title)}`;
|
|
887
|
+
return {
|
|
888
|
+
index,
|
|
889
|
+
title: block.title,
|
|
890
|
+
content: block.content,
|
|
891
|
+
type,
|
|
892
|
+
uri
|
|
893
|
+
};
|
|
1129
894
|
});
|
|
1130
|
-
const provider = opts?.embeddingProvider ?? null;
|
|
1131
|
-
const model = opts?.embeddingModel ?? provider?.model;
|
|
1132
|
-
if (!provider || !model) {
|
|
1133
|
-
return bm25.slice(0, limit);
|
|
1134
|
-
}
|
|
1135
|
-
const embedFn = provider.embedQuery ?? provider.embed;
|
|
1136
|
-
const qVec = Float32Array.from(await embedFn.call(provider, query));
|
|
1137
|
-
const embeddings = listEmbeddings(db, agentId, model);
|
|
1138
|
-
const scored = [];
|
|
1139
|
-
for (const e of embeddings) {
|
|
1140
|
-
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
1141
|
-
}
|
|
1142
|
-
scored.sort((a, b) => b.score - a.score);
|
|
1143
|
-
const semanticTop = scored.slice(0, semanticCandidates);
|
|
1144
|
-
const fused = fuseRrf(
|
|
1145
|
-
[
|
|
1146
|
-
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
1147
|
-
{ name: "semantic", items: semanticTop }
|
|
1148
|
-
],
|
|
1149
|
-
rrfK
|
|
1150
|
-
);
|
|
1151
|
-
const ids = [...fused.keys()];
|
|
1152
|
-
const memories = fetchMemories(db, ids, agentId);
|
|
1153
|
-
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
1154
|
-
const out = [];
|
|
1155
|
-
for (const [id, meta] of fused) {
|
|
1156
|
-
const mem = byId.get(id);
|
|
1157
|
-
if (!mem) continue;
|
|
1158
|
-
out.push({
|
|
1159
|
-
memory: mem,
|
|
1160
|
-
score: meta.score,
|
|
1161
|
-
matchReason: meta.sources.sort().join("+")
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
out.sort((a, b) => b.score - a.score);
|
|
1165
|
-
return out.slice(0, limit);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// src/search/providers.ts
|
|
1169
|
-
var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
|
|
1170
|
-
function getDefaultInstruction(model) {
|
|
1171
|
-
const m = model.toLowerCase();
|
|
1172
|
-
if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
|
|
1173
|
-
if (m.includes("gemini")) return null;
|
|
1174
|
-
return null;
|
|
1175
|
-
}
|
|
1176
|
-
function resolveInstruction(model) {
|
|
1177
|
-
const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
|
|
1178
|
-
if (override !== void 0) {
|
|
1179
|
-
const normalized = override.trim();
|
|
1180
|
-
if (!normalized) return null;
|
|
1181
|
-
const lowered = normalized.toLowerCase();
|
|
1182
|
-
if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
|
|
1183
|
-
return normalized;
|
|
1184
|
-
}
|
|
1185
|
-
return getDefaultInstruction(model);
|
|
1186
895
|
}
|
|
1187
|
-
function
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
if (!apiKey) return null;
|
|
1216
|
-
const instruction = resolveInstruction(model);
|
|
1217
|
-
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
1218
|
-
}
|
|
1219
|
-
return null;
|
|
1220
|
-
}
|
|
1221
|
-
function authHeader(apiKey) {
|
|
1222
|
-
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
1223
|
-
}
|
|
1224
|
-
function normalizeEmbedding(e) {
|
|
1225
|
-
if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
|
|
1226
|
-
if (e.length === 0) throw new Error("Invalid embedding: empty");
|
|
1227
|
-
return e.map((x) => {
|
|
1228
|
-
if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
|
|
1229
|
-
return x;
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
function createOpenAIProvider(opts) {
|
|
1233
|
-
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
1234
|
-
const instructionPrefix = opts.instruction ?? null;
|
|
1235
|
-
async function requestEmbedding(input) {
|
|
1236
|
-
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
|
|
1237
|
-
method: "POST",
|
|
1238
|
-
headers: {
|
|
1239
|
-
"content-type": "application/json",
|
|
1240
|
-
authorization: authHeader(opts.apiKey)
|
|
1241
|
-
},
|
|
1242
|
-
body: JSON.stringify({ model: opts.model, input })
|
|
896
|
+
function ingestText(db, options) {
|
|
897
|
+
const extracted = extractIngestItems(options.text, options.source);
|
|
898
|
+
const dryRun = options.dryRun ?? false;
|
|
899
|
+
const agentId = options.agentId ?? "default";
|
|
900
|
+
if (dryRun) {
|
|
901
|
+
return {
|
|
902
|
+
extracted: extracted.length,
|
|
903
|
+
written: 0,
|
|
904
|
+
skipped: extracted.length,
|
|
905
|
+
dry_run: true,
|
|
906
|
+
details: extracted.map((item) => ({
|
|
907
|
+
index: item.index,
|
|
908
|
+
type: item.type,
|
|
909
|
+
uri: item.uri,
|
|
910
|
+
preview: item.content.slice(0, 80)
|
|
911
|
+
}))
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
let written = 0;
|
|
915
|
+
let skipped = 0;
|
|
916
|
+
const details = [];
|
|
917
|
+
for (const item of extracted) {
|
|
918
|
+
const result = syncOne(db, {
|
|
919
|
+
content: item.content,
|
|
920
|
+
type: item.type,
|
|
921
|
+
uri: item.uri,
|
|
922
|
+
source: `auto:${options.source ?? "ingest"}`,
|
|
923
|
+
agent_id: agentId
|
|
1243
924
|
});
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
925
|
+
if (result.action === "added" || result.action === "updated" || result.action === "merged") {
|
|
926
|
+
written++;
|
|
927
|
+
} else {
|
|
928
|
+
skipped++;
|
|
1247
929
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
930
|
+
details.push({
|
|
931
|
+
index: item.index,
|
|
932
|
+
type: item.type,
|
|
933
|
+
uri: item.uri,
|
|
934
|
+
action: result.action,
|
|
935
|
+
reason: result.reason,
|
|
936
|
+
memoryId: result.memoryId
|
|
937
|
+
});
|
|
1250
938
|
}
|
|
1251
939
|
return {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
},
|
|
1258
|
-
async embedQuery(query) {
|
|
1259
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
1260
|
-
}
|
|
940
|
+
extracted: extracted.length,
|
|
941
|
+
written,
|
|
942
|
+
skipped,
|
|
943
|
+
dry_run: false,
|
|
944
|
+
details
|
|
1261
945
|
};
|
|
1262
946
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
947
|
+
|
|
948
|
+
// src/ingest/watcher.ts
|
|
949
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync, watch } from "fs";
|
|
950
|
+
import { join as join2, relative, resolve } from "path";
|
|
951
|
+
function runAutoIngestWatcher(options) {
|
|
952
|
+
const workspaceDir = resolve(options.workspaceDir);
|
|
953
|
+
const memoryDir = join2(workspaceDir, "memory");
|
|
954
|
+
const memoryMdPath = join2(workspaceDir, "MEMORY.md");
|
|
955
|
+
const debounceMs = options.debounceMs ?? 1200;
|
|
956
|
+
const initialScan = options.initialScan ?? true;
|
|
957
|
+
const logger = options.logger ?? console;
|
|
958
|
+
const timers = /* @__PURE__ */ new Map();
|
|
959
|
+
const watchers = [];
|
|
960
|
+
const stats = {
|
|
961
|
+
triggers: 0,
|
|
962
|
+
filesProcessed: 0,
|
|
963
|
+
extracted: 0,
|
|
964
|
+
written: 0,
|
|
965
|
+
skipped: 0,
|
|
966
|
+
errors: 0
|
|
967
|
+
};
|
|
968
|
+
let stopped = false;
|
|
969
|
+
let queue = Promise.resolve();
|
|
970
|
+
const toSource = (absPath) => {
|
|
971
|
+
const rel = relative(workspaceDir, absPath).replace(/\\/g, "/");
|
|
972
|
+
return rel || absPath;
|
|
973
|
+
};
|
|
974
|
+
const isTrackedMarkdownFile = (absPath) => {
|
|
975
|
+
if (!absPath.endsWith(".md")) return false;
|
|
976
|
+
if (resolve(absPath) === memoryMdPath) return true;
|
|
977
|
+
const rel = relative(memoryDir, absPath).replace(/\\/g, "/");
|
|
978
|
+
if (rel.startsWith("..") || rel === "") return false;
|
|
979
|
+
return !rel.includes("/");
|
|
980
|
+
};
|
|
981
|
+
const ingestFile = (absPath, reason) => {
|
|
982
|
+
if (stopped) return;
|
|
983
|
+
if (!existsSync2(absPath)) {
|
|
984
|
+
logger.log(`[auto-ingest] skip missing file: ${toSource(absPath)} (reason=${reason})`);
|
|
985
|
+
return;
|
|
1281
986
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
987
|
+
let isFile = false;
|
|
988
|
+
try {
|
|
989
|
+
isFile = statSync(absPath).isFile();
|
|
990
|
+
} catch (err) {
|
|
991
|
+
stats.errors += 1;
|
|
992
|
+
logger.warn(`[auto-ingest] stat failed for ${toSource(absPath)}: ${String(err)}`);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (!isFile) return;
|
|
996
|
+
try {
|
|
997
|
+
const text = readFileSync2(absPath, "utf-8");
|
|
998
|
+
const source = toSource(absPath);
|
|
999
|
+
const result = ingestText(options.db, {
|
|
1000
|
+
text,
|
|
1001
|
+
source,
|
|
1002
|
+
agentId: options.agentId
|
|
1003
|
+
});
|
|
1004
|
+
stats.filesProcessed += 1;
|
|
1005
|
+
stats.extracted += result.extracted;
|
|
1006
|
+
stats.written += result.written;
|
|
1007
|
+
stats.skipped += result.skipped;
|
|
1008
|
+
logger.log(
|
|
1009
|
+
`[auto-ingest] file=${source} reason=${reason} extracted=${result.extracted} written=${result.written} skipped=${result.skipped}`
|
|
1010
|
+
);
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
stats.errors += 1;
|
|
1013
|
+
logger.error(`[auto-ingest] ingest failed for ${toSource(absPath)}: ${String(err)}`);
|
|
1295
1014
|
}
|
|
1296
1015
|
};
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
if (!apiKey) return null;
|
|
1311
|
-
return createOpenAIRerankProvider({ apiKey, model, baseUrl });
|
|
1312
|
-
}
|
|
1313
|
-
return null;
|
|
1314
|
-
}
|
|
1315
|
-
function createOpenAIRerankProvider(opts) {
|
|
1316
|
-
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
1317
|
-
return {
|
|
1318
|
-
id: "openai-rerank",
|
|
1319
|
-
model: opts.model,
|
|
1320
|
-
async rerank(query, documents) {
|
|
1321
|
-
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/rerank`, {
|
|
1322
|
-
method: "POST",
|
|
1323
|
-
headers: {
|
|
1324
|
-
"content-type": "application/json",
|
|
1325
|
-
authorization: authHeader2(opts.apiKey)
|
|
1326
|
-
},
|
|
1327
|
-
body: JSON.stringify({ model: opts.model, query, documents })
|
|
1016
|
+
const scheduleIngest = (absPath, reason) => {
|
|
1017
|
+
if (stopped) return;
|
|
1018
|
+
if (!isTrackedMarkdownFile(absPath)) return;
|
|
1019
|
+
stats.triggers += 1;
|
|
1020
|
+
const previous = timers.get(absPath);
|
|
1021
|
+
if (previous) clearTimeout(previous);
|
|
1022
|
+
const timer = setTimeout(() => {
|
|
1023
|
+
timers.delete(absPath);
|
|
1024
|
+
queue = queue.then(() => {
|
|
1025
|
+
ingestFile(absPath, reason);
|
|
1026
|
+
}).catch((err) => {
|
|
1027
|
+
stats.errors += 1;
|
|
1028
|
+
logger.error(`[auto-ingest] queue error: ${String(err)}`);
|
|
1328
1029
|
});
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
return
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1030
|
+
}, debounceMs);
|
|
1031
|
+
timers.set(absPath, timer);
|
|
1032
|
+
};
|
|
1033
|
+
const safeWatch = (dir, onEvent) => {
|
|
1034
|
+
if (!existsSync2(dir)) {
|
|
1035
|
+
logger.warn(`[auto-ingest] watch path does not exist, skipping: ${dir}`);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const watcher = watch(dir, { persistent: true }, (eventType, filename) => {
|
|
1040
|
+
if (!filename) return;
|
|
1041
|
+
onEvent(eventType, filename.toString());
|
|
1042
|
+
});
|
|
1043
|
+
watchers.push(watcher);
|
|
1044
|
+
logger.log(`[auto-ingest] watching ${dir}`);
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
stats.errors += 1;
|
|
1047
|
+
logger.error(`[auto-ingest] failed to watch ${dir}: ${String(err)}`);
|
|
1340
1048
|
}
|
|
1341
1049
|
};
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
|
|
1347
|
-
if (!row) return false;
|
|
1348
|
-
if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
|
|
1349
|
-
const model = opts?.model ?? provider.model;
|
|
1350
|
-
const maxChars = opts?.maxChars ?? 2e3;
|
|
1351
|
-
const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
|
|
1352
|
-
const vector = await provider.embed(text);
|
|
1353
|
-
upsertEmbedding(db, {
|
|
1354
|
-
agent_id: row.agent_id,
|
|
1355
|
-
memory_id: row.id,
|
|
1356
|
-
model,
|
|
1357
|
-
vector
|
|
1050
|
+
safeWatch(workspaceDir, (eventType, filename) => {
|
|
1051
|
+
if (filename === "MEMORY.md") {
|
|
1052
|
+
scheduleIngest(join2(workspaceDir, filename), `workspace:${eventType}`);
|
|
1053
|
+
}
|
|
1358
1054
|
});
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
).all(model, agentId, limit);
|
|
1374
|
-
let embedded = 0;
|
|
1375
|
-
for (const r of rows) {
|
|
1376
|
-
const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
|
|
1377
|
-
if (ok) embedded++;
|
|
1055
|
+
safeWatch(memoryDir, (eventType, filename) => {
|
|
1056
|
+
if (filename.endsWith(".md")) {
|
|
1057
|
+
scheduleIngest(join2(memoryDir, filename), `memory:${eventType}`);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
if (initialScan) {
|
|
1061
|
+
scheduleIngest(memoryMdPath, "initial");
|
|
1062
|
+
if (existsSync2(memoryDir)) {
|
|
1063
|
+
for (const file of readdirSync(memoryDir)) {
|
|
1064
|
+
if (file.endsWith(".md")) {
|
|
1065
|
+
scheduleIngest(join2(memoryDir, file), "initial");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1378
1069
|
}
|
|
1379
|
-
return {
|
|
1070
|
+
return {
|
|
1071
|
+
close: () => {
|
|
1072
|
+
if (stopped) return;
|
|
1073
|
+
stopped = true;
|
|
1074
|
+
for (const timer of timers.values()) {
|
|
1075
|
+
clearTimeout(timer);
|
|
1076
|
+
}
|
|
1077
|
+
timers.clear();
|
|
1078
|
+
for (const watcher of watchers) {
|
|
1079
|
+
try {
|
|
1080
|
+
watcher.close();
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
logger.log(
|
|
1085
|
+
`[auto-ingest] stopped triggers=${stats.triggers} files=${stats.filesProcessed} extracted=${stats.extracted} written=${stats.written} skipped=${stats.skipped} errors=${stats.errors}`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1380
1089
|
}
|
|
1381
1090
|
|
|
1382
1091
|
// src/sleep/decay.ts
|
|
@@ -1439,74 +1148,15 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
1439
1148
|
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
1440
1149
|
}
|
|
1441
1150
|
|
|
1442
|
-
// src/sleep/sync.ts
|
|
1443
|
-
function syncOne(db, input) {
|
|
1444
|
-
const memInput = {
|
|
1445
|
-
content: input.content,
|
|
1446
|
-
type: input.type ?? "event",
|
|
1447
|
-
priority: input.priority,
|
|
1448
|
-
emotion_val: input.emotion_val,
|
|
1449
|
-
source: input.source,
|
|
1450
|
-
agent_id: input.agent_id,
|
|
1451
|
-
uri: input.uri
|
|
1452
|
-
};
|
|
1453
|
-
const guardResult = guard(db, memInput);
|
|
1454
|
-
switch (guardResult.action) {
|
|
1455
|
-
case "skip":
|
|
1456
|
-
return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
|
|
1457
|
-
case "add": {
|
|
1458
|
-
const mem = createMemory(db, memInput);
|
|
1459
|
-
if (!mem) return { action: "skipped", reason: "createMemory returned null" };
|
|
1460
|
-
if (input.uri) {
|
|
1461
|
-
try {
|
|
1462
|
-
createPath(db, mem.id, input.uri);
|
|
1463
|
-
} catch {
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
return { action: "added", memoryId: mem.id, reason: guardResult.reason };
|
|
1467
|
-
}
|
|
1468
|
-
case "update": {
|
|
1469
|
-
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
1470
|
-
createSnapshot(db, guardResult.existingId, "update", "sync");
|
|
1471
|
-
updateMemory(db, guardResult.existingId, { content: input.content });
|
|
1472
|
-
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1473
|
-
}
|
|
1474
|
-
case "merge": {
|
|
1475
|
-
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
1476
|
-
return { action: "skipped", reason: "Missing merge data" };
|
|
1477
|
-
}
|
|
1478
|
-
createSnapshot(db, guardResult.existingId, "merge", "sync");
|
|
1479
|
-
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
1480
|
-
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
function syncBatch(db, inputs) {
|
|
1485
|
-
const results = [];
|
|
1486
|
-
const transaction = db.transaction(() => {
|
|
1487
|
-
for (const input of inputs) {
|
|
1488
|
-
results.push(syncOne(db, input));
|
|
1489
|
-
}
|
|
1490
|
-
});
|
|
1491
|
-
transaction();
|
|
1492
|
-
return results;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
1151
|
// src/sleep/tidy.ts
|
|
1496
1152
|
function runTidy(db, opts) {
|
|
1497
1153
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
1498
|
-
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
1499
1154
|
const agentId = opts?.agent_id;
|
|
1500
1155
|
let archived = 0;
|
|
1501
1156
|
let orphansCleaned = 0;
|
|
1502
|
-
let snapshotsPruned = 0;
|
|
1503
1157
|
const transaction = db.transaction(() => {
|
|
1504
1158
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
1505
1159
|
for (const mem of decayed) {
|
|
1506
|
-
try {
|
|
1507
|
-
createSnapshot(db, mem.id, "delete", "tidy");
|
|
1508
|
-
} catch {
|
|
1509
|
-
}
|
|
1510
1160
|
deleteMemory(db, mem.id);
|
|
1511
1161
|
archived++;
|
|
1512
1162
|
}
|
|
@@ -1518,35 +1168,15 @@ function runTidy(db, opts) {
|
|
|
1518
1168
|
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
1519
1169
|
).run();
|
|
1520
1170
|
orphansCleaned = orphans.changes;
|
|
1521
|
-
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
1522
|
-
`SELECT s.memory_id, COUNT(*) as cnt
|
|
1523
|
-
FROM snapshots s
|
|
1524
|
-
JOIN memories m ON m.id = s.memory_id
|
|
1525
|
-
WHERE m.agent_id = ?
|
|
1526
|
-
GROUP BY s.memory_id HAVING cnt > ?`
|
|
1527
|
-
).all(agentId, maxSnapshots) : db.prepare(
|
|
1528
|
-
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
1529
|
-
GROUP BY memory_id HAVING cnt > ?`
|
|
1530
|
-
).all(maxSnapshots);
|
|
1531
|
-
for (const { memory_id } of memoriesWithSnapshots) {
|
|
1532
|
-
const pruned = db.prepare(
|
|
1533
|
-
`DELETE FROM snapshots WHERE id NOT IN (
|
|
1534
|
-
SELECT id FROM snapshots WHERE memory_id = ?
|
|
1535
|
-
ORDER BY created_at DESC LIMIT ?
|
|
1536
|
-
) AND memory_id = ?`
|
|
1537
|
-
).run(memory_id, maxSnapshots, memory_id);
|
|
1538
|
-
snapshotsPruned += pruned.changes;
|
|
1539
|
-
}
|
|
1540
1171
|
});
|
|
1541
1172
|
transaction();
|
|
1542
|
-
return { archived, orphansCleaned
|
|
1173
|
+
return { archived, orphansCleaned };
|
|
1543
1174
|
}
|
|
1544
1175
|
|
|
1545
1176
|
// src/sleep/govern.ts
|
|
1546
1177
|
function runGovern(db, opts) {
|
|
1547
1178
|
const agentId = opts?.agent_id;
|
|
1548
1179
|
let orphanPaths = 0;
|
|
1549
|
-
let orphanLinks = 0;
|
|
1550
1180
|
let emptyMemories = 0;
|
|
1551
1181
|
const transaction = db.transaction(() => {
|
|
1552
1182
|
const pathResult = agentId ? db.prepare(
|
|
@@ -1555,23 +1185,11 @@ function runGovern(db, opts) {
|
|
|
1555
1185
|
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1556
1186
|
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
1557
1187
|
orphanPaths = pathResult.changes;
|
|
1558
|
-
const linkResult = agentId ? db.prepare(
|
|
1559
|
-
`DELETE FROM links WHERE
|
|
1560
|
-
agent_id = ? AND (
|
|
1561
|
-
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
1562
|
-
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
1563
|
-
)`
|
|
1564
|
-
).run(agentId, agentId, agentId) : db.prepare(
|
|
1565
|
-
`DELETE FROM links WHERE
|
|
1566
|
-
source_id NOT IN (SELECT id FROM memories) OR
|
|
1567
|
-
target_id NOT IN (SELECT id FROM memories)`
|
|
1568
|
-
).run();
|
|
1569
|
-
orphanLinks = linkResult.changes;
|
|
1570
1188
|
const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
1571
1189
|
emptyMemories = emptyResult.changes;
|
|
1572
1190
|
});
|
|
1573
1191
|
transaction();
|
|
1574
|
-
return { orphanPaths,
|
|
1192
|
+
return { orphanPaths, emptyMemories };
|
|
1575
1193
|
}
|
|
1576
1194
|
|
|
1577
1195
|
// src/sleep/boot.ts
|
|
@@ -1628,60 +1246,39 @@ function boot(db, opts) {
|
|
|
1628
1246
|
export {
|
|
1629
1247
|
boot,
|
|
1630
1248
|
calculateVitality,
|
|
1631
|
-
|
|
1249
|
+
classifyIngestType,
|
|
1632
1250
|
contentHash,
|
|
1633
1251
|
countMemories,
|
|
1634
|
-
createDashScopeProvider,
|
|
1635
|
-
createLink,
|
|
1636
1252
|
createMemory,
|
|
1637
|
-
createOpenAIProvider,
|
|
1638
|
-
createOpenAIRerankProvider,
|
|
1639
1253
|
createPath,
|
|
1640
|
-
createSnapshot,
|
|
1641
|
-
decodeEmbedding,
|
|
1642
|
-
deleteLink,
|
|
1643
1254
|
deleteMemory,
|
|
1644
1255
|
deletePath,
|
|
1645
|
-
embedMemory,
|
|
1646
|
-
embedMissingForAgent,
|
|
1647
|
-
encodeEmbedding,
|
|
1648
1256
|
exportMemories,
|
|
1257
|
+
extractIngestItems,
|
|
1649
1258
|
getDecayedMemories,
|
|
1650
|
-
getDefaultInstruction,
|
|
1651
|
-
getEmbedding,
|
|
1652
|
-
getEmbeddingProviderFromEnv,
|
|
1653
|
-
getLinks,
|
|
1654
1259
|
getMemory,
|
|
1655
|
-
getOutgoingLinks,
|
|
1656
1260
|
getPath,
|
|
1657
1261
|
getPathByUri,
|
|
1658
1262
|
getPathsByDomain,
|
|
1659
1263
|
getPathsByMemory,
|
|
1660
1264
|
getPathsByPrefix,
|
|
1661
|
-
getRerankerProviderFromEnv,
|
|
1662
|
-
getSnapshot,
|
|
1663
|
-
getSnapshots,
|
|
1664
|
-
getStrategy,
|
|
1665
1265
|
guard,
|
|
1266
|
+
ingestText,
|
|
1666
1267
|
isCountRow,
|
|
1667
|
-
listEmbeddings,
|
|
1668
1268
|
listMemories,
|
|
1669
1269
|
openDatabase,
|
|
1670
1270
|
parseUri,
|
|
1671
1271
|
recordAccess,
|
|
1672
|
-
|
|
1673
|
-
rerankWithProvider,
|
|
1674
|
-
rollback,
|
|
1272
|
+
runAutoIngestWatcher,
|
|
1675
1273
|
runDecay,
|
|
1676
1274
|
runGovern,
|
|
1677
1275
|
runTidy,
|
|
1678
1276
|
searchBM25,
|
|
1679
|
-
|
|
1277
|
+
slugify,
|
|
1278
|
+
splitIngestBlocks,
|
|
1680
1279
|
syncBatch,
|
|
1681
1280
|
syncOne,
|
|
1682
1281
|
tokenize,
|
|
1683
|
-
|
|
1684
|
-
updateMemory,
|
|
1685
|
-
upsertEmbedding
|
|
1282
|
+
updateMemory
|
|
1686
1283
|
};
|
|
1687
1284
|
//# sourceMappingURL=index.js.map
|