@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.
@@ -577,144 +577,6 @@ function exportMemories(db, dirPath, opts) {
577
577
  return { exported, files };
578
578
  }
579
579
 
580
- // src/search/intent.ts
581
- var INTENT_PATTERNS = {
582
- factual: [
583
- // English
584
- /^(what|who|where|which|how much|how many)\b/i,
585
- /\b(name|address|number|password|config|setting)\b/i,
586
- // Chinese - questions about facts
587
- /是(什么|谁|哪|啥)/,
588
- /叫(什么|啥)/,
589
- /(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
590
- /(多少|几个|哪个|哪些|哪里)/,
591
- // Chinese - lookup patterns
592
- /(查一下|找一下|看看|搜一下)/,
593
- /(.+)是什么$/
594
- ],
595
- temporal: [
596
- // English
597
- /^(when|what time|how long)\b/i,
598
- /\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
599
- /\b(first|latest|newest|oldest|previous|next)\b/i,
600
- // Chinese - time expressions
601
- /什么时候/,
602
- /(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
603
- /(几月|几号|几点|多久|多长时间)/,
604
- /(上次|下次|第一次|最后一次|那天|那时)/,
605
- // Date patterns
606
- /\d{4}[-/.]\d{1,2}/,
607
- /\d{1,2}月\d{1,2}[日号]/,
608
- // Chinese - temporal context
609
- /(历史|记录|日志|以来|至今|期间)/
610
- ],
611
- causal: [
612
- // English
613
- /^(why|how come|what caused)\b/i,
614
- /\b(because|due to|reason|cause|result)\b/i,
615
- // Chinese - causal questions
616
- /为(什么|啥|何)/,
617
- /(原因|导致|造成|引起|因为|所以|结果)/,
618
- /(怎么回事|怎么了|咋回事|咋了)/,
619
- /(为啥|凭啥|凭什么)/,
620
- // Chinese - problem/diagnosis
621
- /(出(了|了什么)?问题|报错|失败|出错|bug)/
622
- ],
623
- exploratory: [
624
- // English
625
- /^(how|tell me about|explain|describe|show me)\b/i,
626
- /^(what do you think|what about|any)\b/i,
627
- /\b(overview|summary|list|compare)\b/i,
628
- // Chinese - exploratory
629
- /(怎么样|怎样|如何)/,
630
- /(介绍|说说|讲讲|聊聊|谈谈)/,
631
- /(有哪些|有什么|有没有)/,
632
- /(关于|对于|至于|关联)/,
633
- /(总结|概括|梳理|回顾|盘点)/,
634
- // Chinese - opinion/analysis
635
- /(看法|想法|意见|建议|评价|感觉|觉得)/,
636
- /(对比|比较|区别|差异|优缺点)/
637
- ]
638
- };
639
- var CN_STRUCTURE_BOOSTS = {
640
- factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
641
- temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
642
- causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
643
- exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
644
- };
645
- function classifyIntent(query) {
646
- const scores = {
647
- factual: 0,
648
- exploratory: 0,
649
- temporal: 0,
650
- causal: 0
651
- };
652
- for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
653
- for (const pattern of patterns) {
654
- if (pattern.test(query)) {
655
- scores[intent] += 1;
656
- }
657
- }
658
- }
659
- for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
660
- for (const pattern of patterns) {
661
- if (pattern.test(query)) {
662
- scores[intent] += 0.5;
663
- }
664
- }
665
- }
666
- const tokens = tokenize(query);
667
- const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
668
- if (totalPatternScore === 0 && tokens.length <= 3) {
669
- scores.factual += 1;
670
- }
671
- let maxIntent = "factual";
672
- let maxScore = 0;
673
- for (const [intent, score] of Object.entries(scores)) {
674
- if (score > maxScore) {
675
- maxScore = score;
676
- maxIntent = intent;
677
- }
678
- }
679
- const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
680
- const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
681
- return { intent: maxIntent, confidence };
682
- }
683
- function getStrategy(intent) {
684
- switch (intent) {
685
- case "factual":
686
- return { boostRecent: false, boostPriority: true, limit: 5 };
687
- case "temporal":
688
- return { boostRecent: true, boostPriority: false, limit: 10 };
689
- case "causal":
690
- return { boostRecent: false, boostPriority: false, limit: 10 };
691
- case "exploratory":
692
- return { boostRecent: false, boostPriority: false, limit: 15 };
693
- }
694
- }
695
-
696
- // src/search/rerank.ts
697
- function rerank(results, opts) {
698
- const now2 = Date.now();
699
- const scored = results.map((r) => {
700
- let finalScore = r.score;
701
- if (opts.boostPriority) {
702
- const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
703
- finalScore *= priorityMultiplier;
704
- }
705
- if (opts.boostRecent && r.memory.updated_at) {
706
- const age = now2 - new Date(r.memory.updated_at).getTime();
707
- const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
708
- const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
709
- finalScore *= recencyBoost;
710
- }
711
- finalScore *= Math.max(0.1, r.memory.vitality);
712
- return { ...r, score: finalScore };
713
- });
714
- scored.sort((a, b) => b.score - a.score);
715
- return scored.slice(0, opts.limit);
716
- }
717
-
718
580
  // src/search/bm25.ts
719
581
  function searchBM25(db, query, opts) {
720
582
  const limit = opts?.limit ?? 20;
@@ -766,295 +628,6 @@ function buildFtsQuery(text) {
766
628
  return tokens.map((w) => `"${w}"`).join(" OR ");
767
629
  }
768
630
 
769
- // src/search/embeddings.ts
770
- function encodeEmbedding(vector) {
771
- const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
772
- return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
773
- }
774
- function decodeEmbedding(buf) {
775
- const copy = Buffer.from(buf);
776
- return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
777
- }
778
- function upsertEmbedding(db, input) {
779
- const ts = now();
780
- const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
781
- const blob = encodeEmbedding(vec);
782
- db.prepare(
783
- `INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
784
- VALUES (?, ?, ?, ?, ?, ?, ?)
785
- ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
786
- dim = excluded.dim,
787
- vector = excluded.vector,
788
- updated_at = excluded.updated_at`
789
- ).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
790
- }
791
- function listEmbeddings(db, agent_id, model) {
792
- const rows = db.prepare(
793
- "SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
794
- ).all(agent_id, model);
795
- return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
796
- }
797
-
798
- // src/search/hybrid.ts
799
- function cosine(a, b) {
800
- const n = Math.min(a.length, b.length);
801
- let dot = 0;
802
- let na = 0;
803
- let nb = 0;
804
- for (let i = 0; i < n; i++) {
805
- const x = a[i];
806
- const y = b[i];
807
- dot += x * y;
808
- na += x * x;
809
- nb += y * y;
810
- }
811
- if (na === 0 || nb === 0) return 0;
812
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
813
- }
814
- function rrfScore(rank, k) {
815
- return 1 / (k + rank);
816
- }
817
- function fuseRrf(lists, k) {
818
- const out = /* @__PURE__ */ new Map();
819
- for (const list of lists) {
820
- for (let i = 0; i < list.items.length; i++) {
821
- const it = list.items[i];
822
- const rank = i + 1;
823
- const add = rrfScore(rank, k);
824
- const prev = out.get(it.id);
825
- if (!prev) out.set(it.id, { score: add, sources: [list.name] });
826
- else {
827
- prev.score += add;
828
- if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
829
- }
830
- }
831
- }
832
- return out;
833
- }
834
- function fetchMemories(db, ids, agentId) {
835
- if (ids.length === 0) return [];
836
- const placeholders = ids.map(() => "?").join(", ");
837
- const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
838
- const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
839
- return rows;
840
- }
841
- async function searchHybrid(db, query, opts) {
842
- const agentId = opts?.agent_id ?? "default";
843
- const limit = opts?.limit ?? 10;
844
- const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
845
- const semanticCandidates = opts?.semanticCandidates ?? 50;
846
- const rrfK = opts?.rrfK ?? 60;
847
- const bm25 = searchBM25(db, query, {
848
- agent_id: agentId,
849
- limit: limit * bm25Mult
850
- });
851
- const provider = opts?.embeddingProvider ?? null;
852
- const model = opts?.embeddingModel ?? provider?.model;
853
- if (!provider || !model) {
854
- return bm25.slice(0, limit);
855
- }
856
- const embedFn = provider.embedQuery ?? provider.embed;
857
- const qVec = Float32Array.from(await embedFn.call(provider, query));
858
- const embeddings = listEmbeddings(db, agentId, model);
859
- const scored = [];
860
- for (const e of embeddings) {
861
- scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
862
- }
863
- scored.sort((a, b) => b.score - a.score);
864
- const semanticTop = scored.slice(0, semanticCandidates);
865
- const fused = fuseRrf(
866
- [
867
- { name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
868
- { name: "semantic", items: semanticTop }
869
- ],
870
- rrfK
871
- );
872
- const ids = [...fused.keys()];
873
- const memories = fetchMemories(db, ids, agentId);
874
- const byId = new Map(memories.map((m) => [m.id, m]));
875
- const out = [];
876
- for (const [id, meta] of fused) {
877
- const mem = byId.get(id);
878
- if (!mem) continue;
879
- out.push({
880
- memory: mem,
881
- score: meta.score,
882
- matchReason: meta.sources.sort().join("+")
883
- });
884
- }
885
- out.sort((a, b) => b.score - a.score);
886
- return out.slice(0, limit);
887
- }
888
-
889
- // src/search/providers.ts
890
- var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
891
- function getDefaultInstruction(model) {
892
- const m = model.toLowerCase();
893
- if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
894
- if (m.includes("gemini")) return null;
895
- return null;
896
- }
897
- function resolveInstruction(model) {
898
- const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
899
- if (override !== void 0) {
900
- const normalized = override.trim();
901
- if (!normalized) return null;
902
- const lowered = normalized.toLowerCase();
903
- if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
904
- return normalized;
905
- }
906
- return getDefaultInstruction(model);
907
- }
908
- function buildQueryInput(query, instructionPrefix) {
909
- if (!instructionPrefix) return query;
910
- return `Instruct: ${instructionPrefix}
911
- Query: ${query}`;
912
- }
913
- function getEmbeddingProviderFromEnv() {
914
- const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
915
- if (provider === "none" || provider === "off" || provider === "false") return null;
916
- if (provider === "openai") {
917
- const apiKey = process.env.OPENAI_API_KEY;
918
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
919
- const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
920
- if (!apiKey) return null;
921
- const instruction = resolveInstruction(model);
922
- return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
923
- }
924
- if (provider === "gemini" || provider === "google") {
925
- const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
926
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
927
- const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
928
- if (!apiKey) return null;
929
- const instruction = resolveInstruction(model);
930
- return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
931
- }
932
- if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
933
- const apiKey = process.env.DASHSCOPE_API_KEY;
934
- const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
935
- const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
936
- if (!apiKey) return null;
937
- const instruction = resolveInstruction(model);
938
- return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
939
- }
940
- return null;
941
- }
942
- function authHeader(apiKey) {
943
- return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
944
- }
945
- function normalizeEmbedding(e) {
946
- if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
947
- if (e.length === 0) throw new Error("Invalid embedding: empty");
948
- return e.map((x) => {
949
- if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
950
- return x;
951
- });
952
- }
953
- function createOpenAIProvider(opts) {
954
- const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
955
- const instructionPrefix = opts.instruction ?? null;
956
- async function requestEmbedding(input) {
957
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
958
- method: "POST",
959
- headers: {
960
- "content-type": "application/json",
961
- authorization: authHeader(opts.apiKey)
962
- },
963
- body: JSON.stringify({ model: opts.model, input })
964
- });
965
- if (!resp.ok) {
966
- const body = await resp.text().catch(() => "");
967
- throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
968
- }
969
- const data = await resp.json();
970
- return normalizeEmbedding(data.data?.[0]?.embedding);
971
- }
972
- return {
973
- id: opts.id ?? "openai",
974
- model: opts.model,
975
- instructionPrefix,
976
- async embed(text) {
977
- return requestEmbedding(text);
978
- },
979
- async embedQuery(query) {
980
- return requestEmbedding(buildQueryInput(query, instructionPrefix));
981
- }
982
- };
983
- }
984
- function createDashScopeProvider(opts) {
985
- const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
986
- const instructionPrefix = opts.instruction ?? null;
987
- async function requestEmbedding(text) {
988
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
989
- method: "POST",
990
- headers: {
991
- "content-type": "application/json",
992
- authorization: authHeader(opts.apiKey)
993
- },
994
- body: JSON.stringify({
995
- model: opts.model,
996
- input: { texts: [text] }
997
- })
998
- });
999
- if (!resp.ok) {
1000
- const body = await resp.text().catch(() => "");
1001
- throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
1002
- }
1003
- const data = await resp.json();
1004
- const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
1005
- return normalizeEmbedding(emb);
1006
- }
1007
- return {
1008
- id: "dashscope",
1009
- model: opts.model,
1010
- instructionPrefix,
1011
- async embed(text) {
1012
- return requestEmbedding(text);
1013
- },
1014
- async embedQuery(query) {
1015
- return requestEmbedding(buildQueryInput(query, instructionPrefix));
1016
- }
1017
- };
1018
- }
1019
-
1020
- // src/search/embed.ts
1021
- async function embedMemory(db, memoryId, provider, opts) {
1022
- const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
1023
- if (!row) return false;
1024
- if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
1025
- const model = opts?.model ?? provider.model;
1026
- const maxChars = opts?.maxChars ?? 2e3;
1027
- const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
1028
- const vector = await provider.embed(text);
1029
- upsertEmbedding(db, {
1030
- agent_id: row.agent_id,
1031
- memory_id: row.id,
1032
- model,
1033
- vector
1034
- });
1035
- return true;
1036
- }
1037
- async function embedMissingForAgent(db, provider, opts) {
1038
- const agentId = opts?.agent_id ?? "default";
1039
- const model = opts?.model ?? provider.model;
1040
- const limit = opts?.limit ?? 1e3;
1041
- const rows = db.prepare(
1042
- `SELECT m.id
1043
- FROM memories m
1044
- LEFT JOIN embeddings e
1045
- ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
1046
- WHERE m.agent_id = ? AND e.memory_id IS NULL
1047
- ORDER BY m.updated_at DESC
1048
- LIMIT ?`
1049
- ).all(model, agentId, limit);
1050
- let embedded = 0;
1051
- for (const r of rows) {
1052
- const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
1053
- if (ok) embedded++;
1054
- }
1055
- return { embedded, scanned: rows.length };
1056
- }
1057
-
1058
631
  // src/core/path.ts
1059
632
  var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
1060
633
  function parseUri(uri) {
@@ -1203,33 +776,15 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
1203
776
  ).all(...agentId ? [threshold, agentId] : [threshold]);
1204
777
  }
1205
778
 
1206
- // src/core/snapshot.ts
1207
- function createSnapshot(db, memoryId, action, changedBy) {
1208
- const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
1209
- if (!memory) throw new Error(`Memory not found: ${memoryId}`);
1210
- const id = newId();
1211
- db.prepare(
1212
- `INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
1213
- VALUES (?, ?, ?, ?, ?, ?)`
1214
- ).run(id, memoryId, memory.content, changedBy ?? null, action, now());
1215
- return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
1216
- }
1217
-
1218
779
  // src/sleep/tidy.ts
1219
780
  function runTidy(db, opts) {
1220
781
  const threshold = opts?.vitalityThreshold ?? 0.05;
1221
- const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
1222
782
  const agentId = opts?.agent_id;
1223
783
  let archived = 0;
1224
784
  let orphansCleaned = 0;
1225
- let snapshotsPruned = 0;
1226
785
  const transaction = db.transaction(() => {
1227
786
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
1228
787
  for (const mem of decayed) {
1229
- try {
1230
- createSnapshot(db, mem.id, "delete", "tidy");
1231
- } catch {
1232
- }
1233
788
  deleteMemory(db, mem.id);
1234
789
  archived++;
1235
790
  }
@@ -1241,35 +796,15 @@ function runTidy(db, opts) {
1241
796
  "DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
1242
797
  ).run();
1243
798
  orphansCleaned = orphans.changes;
1244
- const memoriesWithSnapshots = agentId ? db.prepare(
1245
- `SELECT s.memory_id, COUNT(*) as cnt
1246
- FROM snapshots s
1247
- JOIN memories m ON m.id = s.memory_id
1248
- WHERE m.agent_id = ?
1249
- GROUP BY s.memory_id HAVING cnt > ?`
1250
- ).all(agentId, maxSnapshots) : db.prepare(
1251
- `SELECT memory_id, COUNT(*) as cnt FROM snapshots
1252
- GROUP BY memory_id HAVING cnt > ?`
1253
- ).all(maxSnapshots);
1254
- for (const { memory_id } of memoriesWithSnapshots) {
1255
- const pruned = db.prepare(
1256
- `DELETE FROM snapshots WHERE id NOT IN (
1257
- SELECT id FROM snapshots WHERE memory_id = ?
1258
- ORDER BY created_at DESC LIMIT ?
1259
- ) AND memory_id = ?`
1260
- ).run(memory_id, maxSnapshots, memory_id);
1261
- snapshotsPruned += pruned.changes;
1262
- }
1263
799
  });
1264
800
  transaction();
1265
- return { archived, orphansCleaned, snapshotsPruned };
801
+ return { archived, orphansCleaned };
1266
802
  }
1267
803
 
1268
804
  // src/sleep/govern.ts
1269
805
  function runGovern(db, opts) {
1270
806
  const agentId = opts?.agent_id;
1271
807
  let orphanPaths = 0;
1272
- let orphanLinks = 0;
1273
808
  let emptyMemories = 0;
1274
809
  const transaction = db.transaction(() => {
1275
810
  const pathResult = agentId ? db.prepare(
@@ -1278,23 +813,11 @@ function runGovern(db, opts) {
1278
813
  AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
1279
814
  ).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
1280
815
  orphanPaths = pathResult.changes;
1281
- const linkResult = agentId ? db.prepare(
1282
- `DELETE FROM links WHERE
1283
- agent_id = ? AND (
1284
- source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
1285
- target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
1286
- )`
1287
- ).run(agentId, agentId, agentId) : db.prepare(
1288
- `DELETE FROM links WHERE
1289
- source_id NOT IN (SELECT id FROM memories) OR
1290
- target_id NOT IN (SELECT id FROM memories)`
1291
- ).run();
1292
- orphanLinks = linkResult.changes;
1293
816
  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();
1294
817
  emptyMemories = emptyResult.changes;
1295
818
  });
1296
819
  transaction();
1297
- return { orphanPaths, orphanLinks, emptyMemories };
820
+ return { orphanPaths, emptyMemories };
1298
821
  }
1299
822
 
1300
823
  // src/core/guard.ts
@@ -1424,7 +947,6 @@ function syncOne(db, input) {
1424
947
  }
1425
948
  case "update": {
1426
949
  if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
1427
- createSnapshot(db, guardResult.existingId, "update", "sync");
1428
950
  updateMemory(db, guardResult.existingId, { content: input.content });
1429
951
  return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
1430
952
  }
@@ -1432,7 +954,6 @@ function syncOne(db, input) {
1432
954
  if (!guardResult.existingId || !guardResult.mergedContent) {
1433
955
  return { action: "skipped", reason: "Missing merge data" };
1434
956
  }
1435
- createSnapshot(db, guardResult.existingId, "merge", "sync");
1436
957
  updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
1437
958
  return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
1438
959
  }
@@ -1452,27 +973,26 @@ function getAgentId() {
1452
973
  }
1453
974
  function printHelp() {
1454
975
  console.log(`
1455
- \u{1F9E0} AgentMemory v2 \u2014 Sleep-cycle memory for AI agents
976
+ \u{1F9E0} AgentMemory v3 \u2014 Sleep-cycle memory for AI agents
1456
977
 
1457
978
  Usage: agent-memory <command> [options]
1458
979
 
1459
980
  Commands:
1460
- init Create database
1461
- db:migrate Run schema migrations (no-op if up-to-date)
1462
- embed [--limit N] Embed missing memories (requires provider)
981
+ init Create database
982
+ db:migrate Run schema migrations (no-op if up-to-date)
1463
983
  remember <content> [--uri X] [--type T] Store a memory
1464
- recall <query> [--limit N] Search memories
1465
- boot Load identity memories
1466
- status Show statistics
1467
- reflect [decay|tidy|govern|all] Run sleep cycle
1468
- reindex Rebuild FTS index with jieba tokenizer
1469
- migrate <dir> Import from Markdown files
1470
- export <dir> Export memories to Markdown files
1471
- help Show this help
984
+ recall <query> [--limit N] Search memories (BM25 + priority\xD7vitality)
985
+ boot Load identity memories
986
+ status Show statistics
987
+ reflect [decay|tidy|govern|all] Run sleep cycle
988
+ reindex Rebuild FTS index with jieba tokenizer
989
+ migrate <dir> Import from Markdown files
990
+ export <dir> Export memories to Markdown files
991
+ help Show this help
1472
992
 
1473
993
  Environment:
1474
- AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
1475
- AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
994
+ AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
995
+ AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
1476
996
  `);
1477
997
  }
1478
998
  function getFlag(flag) {
@@ -1497,20 +1017,6 @@ async function main() {
1497
1017
  db.close();
1498
1018
  break;
1499
1019
  }
1500
- case "embed": {
1501
- const provider = getEmbeddingProviderFromEnv();
1502
- if (!provider) {
1503
- console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
1504
- process.exit(1);
1505
- }
1506
- const db = openDatabase({ path: getDbPath() });
1507
- const agentId = getAgentId();
1508
- const limit = parseInt(getFlag("--limit") ?? "200");
1509
- const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
1510
- console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
1511
- db.close();
1512
- break;
1513
- }
1514
1020
  case "remember": {
1515
1021
  const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
1516
1022
  if (!content) {
@@ -1521,14 +1027,7 @@ async function main() {
1521
1027
  const uri = getFlag("--uri");
1522
1028
  const type = getFlag("--type") ?? "knowledge";
1523
1029
  const agentId = getAgentId();
1524
- const provider = getEmbeddingProviderFromEnv();
1525
- const result = syncOne(db, { content, type, uri, agent_id: agentId });
1526
- if (provider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
1527
- try {
1528
- await embedMemory(db, result.memoryId, provider, { agent_id: agentId });
1529
- } catch {
1530
- }
1531
- }
1030
+ const result = syncOne(db, { content, type, uri, source: "manual", agent_id: agentId });
1532
1031
  console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
1533
1032
  db.close();
1534
1033
  break;
@@ -1540,16 +1039,17 @@ async function main() {
1540
1039
  process.exit(1);
1541
1040
  }
1542
1041
  const db = openDatabase({ path: getDbPath() });
1543
- const limit = parseInt(getFlag("--limit") ?? "10");
1544
- const { intent } = classifyIntent(query);
1545
- const strategy = getStrategy(intent);
1042
+ const limit = parseInt(getFlag("--limit") ?? "10", 10);
1546
1043
  const agentId = getAgentId();
1547
- const provider = getEmbeddingProviderFromEnv();
1548
- const raw = await searchHybrid(db, query, { agent_id: agentId, embeddingProvider: provider, limit: limit * 2 });
1549
- const results = rerank(raw, { ...strategy, limit });
1550
- console.log(`\u{1F50D} Intent: ${intent} | Results: ${results.length}
1044
+ const raw = searchBM25(db, query, { agent_id: agentId, limit: Math.max(limit * 2, limit) });
1045
+ const weighted = raw.map((r) => {
1046
+ const weight = [4, 3, 2, 1][r.memory.priority] ?? 1;
1047
+ const vitality = Math.max(0.1, r.memory.vitality);
1048
+ return { ...r, score: r.score * weight * vitality };
1049
+ }).sort((a, b) => b.score - a.score).slice(0, limit);
1050
+ console.log(`\u{1F50D} Results: ${weighted.length}
1551
1051
  `);
1552
- for (const r of results) {
1052
+ for (const r of weighted) {
1553
1053
  const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
1554
1054
  const v = (r.memory.vitality * 100).toFixed(0);
1555
1055
  console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
@@ -1578,17 +1078,11 @@ async function main() {
1578
1078
  const stats = countMemories(db, agentId);
1579
1079
  const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
1580
1080
  const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
1581
- const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
1582
- const snaps = db.prepare(
1583
- `SELECT COUNT(*) as c FROM snapshots s
1584
- JOIN memories m ON m.id = s.memory_id
1585
- WHERE m.agent_id = ?`
1586
- ).get(agentId).c;
1587
1081
  console.log("\u{1F9E0} AgentMemory Status\n");
1588
1082
  console.log(` Total memories: ${stats.total}`);
1589
1083
  console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1590
1084
  console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1591
- console.log(` Paths: ${paths} | Links: ${links} | Snapshots: ${snaps}`);
1085
+ console.log(` Paths: ${paths}`);
1592
1086
  console.log(` Low vitality (<10%): ${lowVit}`);
1593
1087
  db.close();
1594
1088
  break;
@@ -1605,11 +1099,11 @@ async function main() {
1605
1099
  }
1606
1100
  if (phase === "tidy" || phase === "all") {
1607
1101
  const r = runTidy(db, { agent_id: agentId });
1608
- console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans, ${r.snapshotsPruned} snapshots pruned`);
1102
+ console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans`);
1609
1103
  }
1610
1104
  if (phase === "govern" || phase === "all") {
1611
1105
  const r = runGovern(db, { agent_id: agentId });
1612
- console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
1106
+ console.log(` Govern: ${r.orphanPaths} paths, ${r.emptyMemories} empty cleaned`);
1613
1107
  }
1614
1108
  db.close();
1615
1109
  break;