@smyslenny/agent-memory 2.1.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/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,389 +778,314 @@ function buildFtsQuery(text) {
880
778
  return tokens.map((w) => `"${w}"`).join(" OR ");
881
779
  }
882
780
 
883
- // src/search/intent.ts
884
- var INTENT_PATTERNS = {
885
- factual: [
886
- // English
887
- /^(what|who|where|which|how much|how many)\b/i,
888
- /\b(name|address|number|password|config|setting)\b/i,
889
- // Chinese - questions about facts
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
- for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
956
- for (const pattern of patterns) {
957
- if (pattern.test(query)) {
958
- scores[intent] += 1;
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
- for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
963
- for (const pattern of patterns) {
964
- if (pattern.test(query)) {
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
- const tokens = tokenize(query);
970
- const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
971
- if (totalPatternScore === 0 && tokens.length <= 3) {
972
- scores.factual += 1;
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
820
  }
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
- }
998
-
999
- // src/search/rerank.ts
1000
- function rerank(results, opts) {
1001
- const now2 = Date.now();
1002
- const scored = results.map((r) => {
1003
- let finalScore = r.score;
1004
- if (opts.boostPriority) {
1005
- const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
1006
- finalScore *= priorityMultiplier;
1007
- }
1008
- if (opts.boostRecent && r.memory.updated_at) {
1009
- const age = now2 - new Date(r.memory.updated_at).getTime();
1010
- const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
1011
- const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
1012
- 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));
1013
826
  }
1014
- finalScore *= Math.max(0.1, r.memory.vitality);
1015
- return { ...r, score: finalScore };
1016
827
  });
1017
- scored.sort((a, b) => b.score - a.score);
1018
- return scored.slice(0, opts.limit);
1019
- }
1020
-
1021
- // src/search/embeddings.ts
1022
- function encodeEmbedding(vector) {
1023
- const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
1024
- return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
1025
- }
1026
- function decodeEmbedding(buf) {
1027
- const copy = Buffer.from(buf);
1028
- return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
1029
- }
1030
- function upsertEmbedding(db, input) {
1031
- const ts = now();
1032
- const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
1033
- const blob = encodeEmbedding(vec);
1034
- db.prepare(
1035
- `INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
1036
- VALUES (?, ?, ?, ?, ?, ?, ?)
1037
- ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
1038
- dim = excluded.dim,
1039
- vector = excluded.vector,
1040
- updated_at = excluded.updated_at`
1041
- ).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
1042
- }
1043
- function getEmbedding(db, agent_id, memory_id, model) {
1044
- const row = db.prepare(
1045
- "SELECT agent_id, memory_id, model, dim, vector, created_at, updated_at FROM embeddings WHERE agent_id = ? AND memory_id = ? AND model = ?"
1046
- ).get(agent_id, memory_id, model);
1047
- if (!row) return null;
1048
- return { ...row, vector: decodeEmbedding(row.vector) };
1049
- }
1050
- function listEmbeddings(db, agent_id, model) {
1051
- const rows = db.prepare(
1052
- "SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
1053
- ).all(agent_id, model);
1054
- return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
828
+ transaction();
829
+ return results;
1055
830
  }
1056
831
 
1057
- // src/search/hybrid.ts
1058
- function cosine(a, b) {
1059
- const n = Math.min(a.length, b.length);
1060
- let dot = 0;
1061
- let na = 0;
1062
- let nb = 0;
1063
- for (let i = 0; i < n; i++) {
1064
- const x = a[i];
1065
- const y = b[i];
1066
- dot += x * y;
1067
- na += x * x;
1068
- nb += y * y;
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";
1069
840
  }
1070
- if (na === 0 || nb === 0) return 0;
1071
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
1072
- }
1073
- function rrfScore(rank, k) {
1074
- return 1 / (k + rank);
1075
- }
1076
- function fuseRrf(lists, k) {
1077
- const out = /* @__PURE__ */ new Map();
1078
- for (const list of lists) {
1079
- for (let i = 0; i < list.items.length; i++) {
1080
- const it = list.items[i];
1081
- const rank = i + 1;
1082
- const add = rrfScore(rank, k);
1083
- const prev = out.get(it.id);
1084
- if (!prev) out.set(it.id, { score: add, sources: [list.name] });
1085
- else {
1086
- prev.score += add;
1087
- if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
1088
- }
1089
- }
1090
- }
1091
- return out;
1092
- }
1093
- function fetchMemories(db, ids, agentId) {
1094
- if (ids.length === 0) return [];
1095
- const placeholders = ids.map(() => "?").join(", ");
1096
- const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
1097
- const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
1098
- return rows;
1099
- }
1100
- async function searchHybrid(db, query, opts) {
1101
- const agentId = opts?.agent_id ?? "default";
1102
- const limit = opts?.limit ?? 10;
1103
- const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
1104
- const semanticCandidates = opts?.semanticCandidates ?? 50;
1105
- const rrfK = opts?.rrfK ?? 60;
1106
- const bm25 = searchBM25(db, query, {
1107
- agent_id: agentId,
1108
- limit: limit * bm25Mult
1109
- });
1110
- const provider = opts?.embeddingProvider ?? null;
1111
- const model = opts?.embeddingModel ?? provider?.model;
1112
- if (!provider || !model) {
1113
- return bm25.slice(0, limit);
841
+ if (/##\s*情感|❤️|💕|爱你|感动|难过|开心|害怕|想念|表白/.test(text)) {
842
+ return "emotion";
1114
843
  }
1115
- const qVec = Float32Array.from(await provider.embed(query));
1116
- const embeddings = listEmbeddings(db, agentId, model);
1117
- const scored = [];
1118
- for (const e of embeddings) {
1119
- scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
844
+ if (/##\s*决策|##\s*技术|选型|教训|\bknowledge\b|⚠️|复盘|经验/.test(text)) {
845
+ return "knowledge";
1120
846
  }
1121
- scored.sort((a, b) => b.score - a.score);
1122
- const semanticTop = scored.slice(0, semanticCandidates);
1123
- const fused = fuseRrf(
1124
- [
1125
- { name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
1126
- { name: "semantic", items: semanticTop }
1127
- ],
1128
- rrfK
1129
- );
1130
- const ids = [...fused.keys()];
1131
- const memories = fetchMemories(db, ids, agentId);
1132
- const byId = new Map(memories.map((m) => [m.id, m]));
1133
- const out = [];
1134
- for (const [id, meta] of fused) {
1135
- const mem = byId.get(id);
1136
- if (!mem) continue;
1137
- out.push({
1138
- memory: mem,
1139
- score: meta.score,
1140
- matchReason: meta.sources.sort().join("+")
1141
- });
847
+ if (/\d{4}-\d{2}-\d{2}|发生了|完成了|今天|昨日|刚刚|部署|上线/.test(text)) {
848
+ return "event";
1142
849
  }
1143
- out.sort((a, b) => b.score - a.score);
1144
- return out.slice(0, limit);
1145
- }
1146
-
1147
- // src/search/providers.ts
1148
- function getEmbeddingProviderFromEnv() {
1149
- const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
1150
- if (provider === "none" || provider === "off" || provider === "false") return null;
1151
- if (provider === "openai") {
1152
- const apiKey = process.env.OPENAI_API_KEY;
1153
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
1154
- const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
1155
- if (!apiKey) return null;
1156
- return createOpenAIProvider({ apiKey, model, baseUrl });
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 });
867
+ }
868
+ return blocks;
1157
869
  }
1158
- if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
1159
- const apiKey = process.env.DASHSCOPE_API_KEY;
1160
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
1161
- const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
1162
- if (!apiKey) return null;
1163
- return createDashScopeProvider({ apiKey, model, baseUrl });
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 }));
1164
873
  }
1165
- return null;
1166
- }
1167
- function authHeader(apiKey) {
1168
- return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
1169
- }
1170
- function normalizeEmbedding(e) {
1171
- if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
1172
- if (e.length === 0) throw new Error("Invalid embedding: empty");
1173
- return e.map((x) => {
1174
- if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
1175
- return x;
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
+ };
1176
894
  });
1177
895
  }
1178
- function createOpenAIProvider(opts) {
1179
- const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
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
924
+ });
925
+ if (result.action === "added" || result.action === "updated" || result.action === "merged") {
926
+ written++;
927
+ } else {
928
+ skipped++;
929
+ }
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
+ });
938
+ }
1180
939
  return {
1181
- id: "openai",
1182
- model: opts.model,
1183
- async embed(text) {
1184
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
1185
- method: "POST",
1186
- headers: {
1187
- "content-type": "application/json",
1188
- authorization: authHeader(opts.apiKey)
1189
- },
1190
- body: JSON.stringify({ model: opts.model, input: text })
940
+ extracted: extracted.length,
941
+ written,
942
+ skipped,
943
+ dry_run: false,
944
+ details
945
+ };
946
+ }
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;
986
+ }
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
1191
1003
  });
1192
- if (!resp.ok) {
1193
- const body = await resp.text().catch(() => "");
1194
- throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
1195
- }
1196
- const data = await resp.json();
1197
- const emb = data.data?.[0]?.embedding;
1198
- return normalizeEmbedding(emb);
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)}`);
1199
1014
  }
1200
1015
  };
1201
- }
1202
- function createDashScopeProvider(opts) {
1203
- const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
1204
- return {
1205
- id: "dashscope",
1206
- model: opts.model,
1207
- async embed(text) {
1208
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
1209
- method: "POST",
1210
- headers: {
1211
- "content-type": "application/json",
1212
- authorization: authHeader(opts.apiKey)
1213
- },
1214
- body: JSON.stringify({
1215
- model: opts.model,
1216
- input: { texts: [text] }
1217
- })
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)}`);
1218
1029
  });
1219
- if (!resp.ok) {
1220
- const body = await resp.text().catch(() => "");
1221
- throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
1222
- }
1223
- const data = await resp.json();
1224
- const emb = data?.output?.embeddings?.[0]?.embedding ?? data?.output?.embeddings?.[0]?.vector ?? data?.output?.embedding ?? data?.data?.[0]?.embedding;
1225
- return normalizeEmbedding(emb);
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)}`);
1226
1048
  }
1227
1049
  };
1228
- }
1229
-
1230
- // src/search/embed.ts
1231
- async function embedMemory(db, memoryId, provider, opts) {
1232
- const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
1233
- if (!row) return false;
1234
- if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
1235
- const model = opts?.model ?? provider.model;
1236
- const maxChars = opts?.maxChars ?? 2e3;
1237
- const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
1238
- const vector = await provider.embed(text);
1239
- upsertEmbedding(db, {
1240
- agent_id: row.agent_id,
1241
- memory_id: row.id,
1242
- model,
1243
- vector
1050
+ safeWatch(workspaceDir, (eventType, filename) => {
1051
+ if (filename === "MEMORY.md") {
1052
+ scheduleIngest(join2(workspaceDir, filename), `workspace:${eventType}`);
1053
+ }
1244
1054
  });
1245
- return true;
1246
- }
1247
- async function embedMissingForAgent(db, provider, opts) {
1248
- const agentId = opts?.agent_id ?? "default";
1249
- const model = opts?.model ?? provider.model;
1250
- const limit = opts?.limit ?? 1e3;
1251
- const rows = db.prepare(
1252
- `SELECT m.id
1253
- FROM memories m
1254
- LEFT JOIN embeddings e
1255
- ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
1256
- WHERE m.agent_id = ? AND e.memory_id IS NULL
1257
- ORDER BY m.updated_at DESC
1258
- LIMIT ?`
1259
- ).all(model, agentId, limit);
1260
- let embedded = 0;
1261
- for (const r of rows) {
1262
- const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
1263
- 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
+ }
1264
1069
  }
1265
- return { embedded, scanned: rows.length };
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
+ };
1266
1089
  }
1267
1090
 
1268
1091
  // src/sleep/decay.ts
@@ -1325,74 +1148,15 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
1325
1148
  ).all(...agentId ? [threshold, agentId] : [threshold]);
1326
1149
  }
1327
1150
 
1328
- // src/sleep/sync.ts
1329
- function syncOne(db, input) {
1330
- const memInput = {
1331
- content: input.content,
1332
- type: input.type ?? "event",
1333
- priority: input.priority,
1334
- emotion_val: input.emotion_val,
1335
- source: input.source,
1336
- agent_id: input.agent_id,
1337
- uri: input.uri
1338
- };
1339
- const guardResult = guard(db, memInput);
1340
- switch (guardResult.action) {
1341
- case "skip":
1342
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
1343
- case "add": {
1344
- const mem = createMemory(db, memInput);
1345
- if (!mem) return { action: "skipped", reason: "createMemory returned null" };
1346
- if (input.uri) {
1347
- try {
1348
- createPath(db, mem.id, input.uri);
1349
- } catch {
1350
- }
1351
- }
1352
- return { action: "added", memoryId: mem.id, reason: guardResult.reason };
1353
- }
1354
- case "update": {
1355
- if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
1356
- createSnapshot(db, guardResult.existingId, "update", "sync");
1357
- updateMemory(db, guardResult.existingId, { content: input.content });
1358
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
1359
- }
1360
- case "merge": {
1361
- if (!guardResult.existingId || !guardResult.mergedContent) {
1362
- return { action: "skipped", reason: "Missing merge data" };
1363
- }
1364
- createSnapshot(db, guardResult.existingId, "merge", "sync");
1365
- updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
1366
- return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
1367
- }
1368
- }
1369
- }
1370
- function syncBatch(db, inputs) {
1371
- const results = [];
1372
- const transaction = db.transaction(() => {
1373
- for (const input of inputs) {
1374
- results.push(syncOne(db, input));
1375
- }
1376
- });
1377
- transaction();
1378
- return results;
1379
- }
1380
-
1381
1151
  // src/sleep/tidy.ts
1382
1152
  function runTidy(db, opts) {
1383
1153
  const threshold = opts?.vitalityThreshold ?? 0.05;
1384
- const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
1385
1154
  const agentId = opts?.agent_id;
1386
1155
  let archived = 0;
1387
1156
  let orphansCleaned = 0;
1388
- let snapshotsPruned = 0;
1389
1157
  const transaction = db.transaction(() => {
1390
1158
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
1391
1159
  for (const mem of decayed) {
1392
- try {
1393
- createSnapshot(db, mem.id, "delete", "tidy");
1394
- } catch {
1395
- }
1396
1160
  deleteMemory(db, mem.id);
1397
1161
  archived++;
1398
1162
  }
@@ -1404,35 +1168,15 @@ function runTidy(db, opts) {
1404
1168
  "DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
1405
1169
  ).run();
1406
1170
  orphansCleaned = orphans.changes;
1407
- const memoriesWithSnapshots = agentId ? db.prepare(
1408
- `SELECT s.memory_id, COUNT(*) as cnt
1409
- FROM snapshots s
1410
- JOIN memories m ON m.id = s.memory_id
1411
- WHERE m.agent_id = ?
1412
- GROUP BY s.memory_id HAVING cnt > ?`
1413
- ).all(agentId, maxSnapshots) : db.prepare(
1414
- `SELECT memory_id, COUNT(*) as cnt FROM snapshots
1415
- GROUP BY memory_id HAVING cnt > ?`
1416
- ).all(maxSnapshots);
1417
- for (const { memory_id } of memoriesWithSnapshots) {
1418
- const pruned = db.prepare(
1419
- `DELETE FROM snapshots WHERE id NOT IN (
1420
- SELECT id FROM snapshots WHERE memory_id = ?
1421
- ORDER BY created_at DESC LIMIT ?
1422
- ) AND memory_id = ?`
1423
- ).run(memory_id, maxSnapshots, memory_id);
1424
- snapshotsPruned += pruned.changes;
1425
- }
1426
1171
  });
1427
1172
  transaction();
1428
- return { archived, orphansCleaned, snapshotsPruned };
1173
+ return { archived, orphansCleaned };
1429
1174
  }
1430
1175
 
1431
1176
  // src/sleep/govern.ts
1432
1177
  function runGovern(db, opts) {
1433
1178
  const agentId = opts?.agent_id;
1434
1179
  let orphanPaths = 0;
1435
- let orphanLinks = 0;
1436
1180
  let emptyMemories = 0;
1437
1181
  const transaction = db.transaction(() => {
1438
1182
  const pathResult = agentId ? db.prepare(
@@ -1441,23 +1185,11 @@ function runGovern(db, opts) {
1441
1185
  AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
1442
1186
  ).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
1443
1187
  orphanPaths = pathResult.changes;
1444
- const linkResult = agentId ? db.prepare(
1445
- `DELETE FROM links WHERE
1446
- agent_id = ? AND (
1447
- source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
1448
- target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
1449
- )`
1450
- ).run(agentId, agentId, agentId) : db.prepare(
1451
- `DELETE FROM links WHERE
1452
- source_id NOT IN (SELECT id FROM memories) OR
1453
- target_id NOT IN (SELECT id FROM memories)`
1454
- ).run();
1455
- orphanLinks = linkResult.changes;
1456
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();
1457
1189
  emptyMemories = emptyResult.changes;
1458
1190
  });
1459
1191
  transaction();
1460
- return { orphanPaths, orphanLinks, emptyMemories };
1192
+ return { orphanPaths, emptyMemories };
1461
1193
  }
1462
1194
 
1463
1195
  // src/sleep/boot.ts
@@ -1514,56 +1246,39 @@ function boot(db, opts) {
1514
1246
  export {
1515
1247
  boot,
1516
1248
  calculateVitality,
1517
- classifyIntent,
1249
+ classifyIngestType,
1518
1250
  contentHash,
1519
1251
  countMemories,
1520
- createDashScopeProvider,
1521
- createLink,
1522
1252
  createMemory,
1523
- createOpenAIProvider,
1524
1253
  createPath,
1525
- createSnapshot,
1526
- decodeEmbedding,
1527
- deleteLink,
1528
1254
  deleteMemory,
1529
1255
  deletePath,
1530
- embedMemory,
1531
- embedMissingForAgent,
1532
- encodeEmbedding,
1533
1256
  exportMemories,
1257
+ extractIngestItems,
1534
1258
  getDecayedMemories,
1535
- getEmbedding,
1536
- getEmbeddingProviderFromEnv,
1537
- getLinks,
1538
1259
  getMemory,
1539
- getOutgoingLinks,
1540
1260
  getPath,
1541
1261
  getPathByUri,
1542
1262
  getPathsByDomain,
1543
1263
  getPathsByMemory,
1544
1264
  getPathsByPrefix,
1545
- getSnapshot,
1546
- getSnapshots,
1547
- getStrategy,
1548
1265
  guard,
1266
+ ingestText,
1549
1267
  isCountRow,
1550
- listEmbeddings,
1551
1268
  listMemories,
1552
1269
  openDatabase,
1553
1270
  parseUri,
1554
1271
  recordAccess,
1555
- rerank,
1556
- rollback,
1272
+ runAutoIngestWatcher,
1557
1273
  runDecay,
1558
1274
  runGovern,
1559
1275
  runTidy,
1560
1276
  searchBM25,
1561
- searchHybrid,
1277
+ slugify,
1278
+ splitIngestBlocks,
1562
1279
  syncBatch,
1563
1280
  syncOne,
1564
1281
  tokenize,
1565
- traverse,
1566
- updateMemory,
1567
- upsertEmbedding
1282
+ updateMemory
1568
1283
  };
1569
1284
  //# sourceMappingURL=index.js.map