ziiagentmemory 0.1.0 → 0.3.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.
@@ -0,0 +1,206 @@
1
+ import { createRequire } from "node:module";
2
+ import { createHash } from "node:crypto";
3
+ //#region src/state/cjk-segmenter.ts
4
+ const cjkRequire = createRequire(import.meta.url);
5
+ const CJK_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
6
+ const KANA_RE = /[\p{Script=Hiragana}\p{Script=Katakana}]/u;
7
+ const HANGUL_RE = /\p{Script=Hangul}/u;
8
+ const CJK_RUN_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
9
+ const HANGUL_BLOCK_RE = /[가-힯]+/g;
10
+ const hintShown = /* @__PURE__ */ new Set();
11
+ function hasCjk(text) {
12
+ return CJK_RE.test(text);
13
+ }
14
+ function showHintOnce(key, message) {
15
+ if (hintShown.has(key)) return;
16
+ hintShown.add(key);
17
+ if (typeof process !== "undefined" && process.stderr?.write) process.stderr.write(`agentmemory: ${message}\n`);
18
+ }
19
+ let jiebaInstance = null;
20
+ let jiebaLoaded = false;
21
+ function getJieba() {
22
+ if (jiebaLoaded) return jiebaInstance;
23
+ jiebaLoaded = true;
24
+ try {
25
+ const mod = cjkRequire("@node-rs/jieba");
26
+ try {
27
+ const dictMod = cjkRequire("@node-rs/jieba/dict");
28
+ jiebaInstance = mod.Jieba.withDict(dictMod.dict);
29
+ } catch {
30
+ jiebaInstance = new mod.Jieba();
31
+ }
32
+ return jiebaInstance;
33
+ } catch {
34
+ showHintOnce("jieba", "install @node-rs/jieba to improve Chinese search; falling back to whole-string tokenization");
35
+ return null;
36
+ }
37
+ }
38
+ let jaSegmenterInstance = null;
39
+ let jaSegmenterLoaded = false;
40
+ function getJaSegmenter() {
41
+ if (jaSegmenterLoaded) return jaSegmenterInstance;
42
+ jaSegmenterLoaded = true;
43
+ try {
44
+ jaSegmenterInstance = new (cjkRequire("tiny-segmenter"))();
45
+ return jaSegmenterInstance;
46
+ } catch {
47
+ showHintOnce("tiny-segmenter", "install tiny-segmenter to improve Japanese search; falling back to whole-string tokenization");
48
+ return null;
49
+ }
50
+ }
51
+ function cleanTokens(tokens) {
52
+ const out = [];
53
+ for (const t of tokens) {
54
+ const trimmed = t.trim();
55
+ if (trimmed) out.push(trimmed);
56
+ }
57
+ return out;
58
+ }
59
+ function segmentHan(text) {
60
+ const j = getJieba();
61
+ if (!j) return [text];
62
+ try {
63
+ return cleanTokens(j.cut(text, true));
64
+ } catch {
65
+ return [text];
66
+ }
67
+ }
68
+ function segmentKana(text) {
69
+ const s = getJaSegmenter();
70
+ if (!s) return [text];
71
+ try {
72
+ return cleanTokens(s.segment(text));
73
+ } catch {
74
+ return [text];
75
+ }
76
+ }
77
+ function segmentHangul(text) {
78
+ const out = [];
79
+ for (const m of text.matchAll(HANGUL_BLOCK_RE)) if (m[0]) out.push(m[0]);
80
+ return out;
81
+ }
82
+ function segmentCjk(text) {
83
+ if (!hasCjk(text)) return [text];
84
+ const out = [];
85
+ let cursor = 0;
86
+ for (const m of text.matchAll(CJK_RUN_RE)) {
87
+ const start = m.index ?? 0;
88
+ const run = m[0];
89
+ const end = start + run.length;
90
+ if (start > cursor) {
91
+ const piece = text.slice(cursor, start).trim();
92
+ if (piece) out.push(piece);
93
+ }
94
+ if (HANGUL_RE.test(run)) out.push(...segmentHangul(run));
95
+ else if (KANA_RE.test(run)) out.push(...segmentKana(run));
96
+ else out.push(...segmentHan(run));
97
+ cursor = end;
98
+ }
99
+ if (cursor < text.length) {
100
+ const trailing = text.slice(cursor).trim();
101
+ if (trailing) out.push(trailing);
102
+ }
103
+ return out;
104
+ }
105
+ //#endregion
106
+ //#region src/state/schema.ts
107
+ const KV = {
108
+ sessions: "mem:sessions",
109
+ observations: (sessionId) => `mem:obs:${sessionId}`,
110
+ memories: "mem:memories",
111
+ summaries: "mem:summaries",
112
+ config: "mem:config",
113
+ metrics: "mem:metrics",
114
+ health: "mem:health",
115
+ embeddings: (obsId) => `mem:emb:${obsId}`,
116
+ bm25Index: "mem:index:bm25",
117
+ relations: "mem:relations",
118
+ profiles: "mem:profiles",
119
+ claudeBridge: "mem:claude-bridge",
120
+ graphNodes: "mem:graph:nodes",
121
+ graphEdges: "mem:graph:edges",
122
+ graphSnapshot: "mem:graph:snapshot",
123
+ graphNameIndex: "mem:graph:name-index",
124
+ graphEdgeKey: "mem:graph:edge-key",
125
+ graphNodeDegree: "mem:graph:node-degree",
126
+ semantic: "mem:semantic",
127
+ procedural: "mem:procedural",
128
+ teamShared: (teamId) => `mem:team:${teamId}:shared`,
129
+ teamUsers: (teamId, userId) => `mem:team:${teamId}:users:${userId}`,
130
+ teamProfile: (teamId) => `mem:team:${teamId}:profile`,
131
+ audit: "mem:audit",
132
+ actions: "mem:actions",
133
+ actionEdges: "mem:action-edges",
134
+ leases: "mem:leases",
135
+ routines: "mem:routines",
136
+ routineRuns: "mem:routine-runs",
137
+ signals: "mem:signals",
138
+ checkpoints: "mem:checkpoints",
139
+ mesh: "mem:mesh",
140
+ sketches: "mem:sketches",
141
+ facets: "mem:facets",
142
+ sentinels: "mem:sentinels",
143
+ crystals: "mem:crystals",
144
+ lessons: "mem:lessons",
145
+ insights: "mem:insights",
146
+ graphEdgeHistory: "mem:graph:edge-history",
147
+ enrichedChunks: (sessionId) => `mem:enriched:${sessionId}`,
148
+ latentEmbeddings: (obsId) => `mem:latent:${obsId}`,
149
+ retentionScores: "mem:retention",
150
+ accessLog: "mem:access",
151
+ imageRefs: "mem:image-refs",
152
+ imageEmbeddings: "mem:image-embeddings",
153
+ slots: "mem:slots",
154
+ globalSlots: "mem:slots:global",
155
+ state: "mem:state",
156
+ commits: "mem:commits",
157
+ recentSearches: "mem:recent-searches"
158
+ };
159
+ const STREAM = {
160
+ name: "mem-live",
161
+ group: (sessionId) => sessionId,
162
+ viewerGroup: "viewer"
163
+ };
164
+ function generateId(prefix) {
165
+ return `${prefix}_${Date.now().toString(36)}_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
166
+ }
167
+ function fingerprintId(prefix, content) {
168
+ return `${prefix}_${createHash("sha256").update(content).digest("hex").slice(0, 16)}`;
169
+ }
170
+ /**
171
+ * CJK-aware tokenizer for Jaccard similarity.
172
+ *
173
+ * For English/whitespace-delimited text we keep the original behaviour:
174
+ * split on whitespace, filter tokens longer than 2 characters.
175
+ *
176
+ * For CJK text (Chinese/Japanese/Korean) whitespace splitting produces
177
+ * near-disjoint sets because CJK text is typically written without spaces.
178
+ * We reuse the existing `segmentCjk` function and additionally emit
179
+ * character bigrams from CJK runs so that no-space Chinese text still
180
+ * produces useful overlap.
181
+ */
182
+ function tokenizeForJaccard(text) {
183
+ if (!hasCjk(text)) return new Set(text.split(/\s+/).filter((t) => t.length > 2));
184
+ const segments = segmentCjk(text);
185
+ const tokens = /* @__PURE__ */ new Set();
186
+ for (const seg of segments) {
187
+ const cleaned = seg.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase();
188
+ if (cleaned.length > 2) tokens.add(cleaned);
189
+ const cjkChars = seg.match(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
190
+ if (cjkChars && cjkChars.length >= 2) for (let i = 0; i < cjkChars.length - 1; i++) tokens.add(cjkChars[i] + cjkChars[i + 1]);
191
+ }
192
+ return tokens;
193
+ }
194
+ function jaccardSimilarity(a, b) {
195
+ const setA = tokenizeForJaccard(a);
196
+ const setB = tokenizeForJaccard(b);
197
+ if (setA.size === 0 && setB.size === 0) return 1;
198
+ if (setA.size === 0 || setB.size === 0) return 0;
199
+ let intersection = 0;
200
+ for (const word of setA) if (setB.has(word)) intersection++;
201
+ return intersection / (setA.size + setB.size - intersection);
202
+ }
203
+ //#endregion
204
+ export { jaccardSimilarity as a, generateId as i, STREAM as n, hasCjk as o, fingerprintId as r, segmentCjk as s, KV as t };
205
+
206
+ //# sourceMappingURL=schema-BQLY6d8C.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-BQLY6d8C.mjs","names":[],"sources":["../src/state/cjk-segmenter.ts","../src/state/schema.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\r\n\r\nconst cjkRequire = createRequire(import.meta.url);\r\n\r\nconst CJK_RE = /[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]/u;\r\nconst HAN_RE = /\\p{Script=Han}/u;\r\nconst KANA_RE = /[\\p{Script=Hiragana}\\p{Script=Katakana}]/u;\r\nconst HANGUL_RE = /\\p{Script=Hangul}/u;\r\nconst CJK_RUN_RE = /[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]+/gu;\r\nconst HANGUL_BLOCK_RE = /[가-힯]+/g;\r\n\r\ntype Script = \"han\" | \"kana\" | \"hangul\" | \"other\";\r\n\r\nconst hintShown = new Set<string>();\r\n\r\nexport function hasCjk(text: string): boolean {\r\n return CJK_RE.test(text);\r\n}\r\n\r\nexport function detectScript(text: string): Script {\r\n if (HAN_RE.test(text)) return \"han\";\r\n if (KANA_RE.test(text)) return \"kana\";\r\n if (HANGUL_RE.test(text)) return \"hangul\";\r\n return \"other\";\r\n}\r\n\r\nfunction showHintOnce(key: string, message: string): void {\r\n if (hintShown.has(key)) return;\r\n hintShown.add(key);\r\n if (typeof process !== \"undefined\" && process.stderr?.write) {\r\n process.stderr.write(`agentmemory: ${message}\\n`);\r\n }\r\n}\r\n\r\ninterface JiebaInstance {\r\n cut(text: string, hmm?: boolean): string[];\r\n}\r\n\r\nlet jiebaInstance: JiebaInstance | null = null;\r\nlet jiebaLoaded = false;\r\n\r\nfunction getJieba(): JiebaInstance | null {\r\n if (jiebaLoaded) return jiebaInstance;\r\n jiebaLoaded = true;\r\n try {\r\n const mod = cjkRequire(\"@node-rs/jieba\") as {\r\n Jieba: {\r\n new (): JiebaInstance;\r\n withDict(dict: Uint8Array): JiebaInstance;\r\n };\r\n };\r\n try {\r\n const dictMod = cjkRequire(\"@node-rs/jieba/dict\") as { dict: Uint8Array };\r\n jiebaInstance = mod.Jieba.withDict(dictMod.dict);\r\n } catch {\r\n jiebaInstance = new mod.Jieba();\r\n }\r\n return jiebaInstance;\r\n } catch {\r\n showHintOnce(\r\n \"jieba\",\r\n \"install @node-rs/jieba to improve Chinese search; falling back to whole-string tokenization\",\r\n );\r\n return null;\r\n }\r\n}\r\n\r\ninterface JaSegmenter {\r\n segment(text: string): string[];\r\n}\r\n\r\nlet jaSegmenterInstance: JaSegmenter | null = null;\r\nlet jaSegmenterLoaded = false;\r\n\r\nfunction getJaSegmenter(): JaSegmenter | null {\r\n if (jaSegmenterLoaded) return jaSegmenterInstance;\r\n jaSegmenterLoaded = true;\r\n try {\r\n const Ctor = cjkRequire(\"tiny-segmenter\") as new () => JaSegmenter;\r\n jaSegmenterInstance = new Ctor();\r\n return jaSegmenterInstance;\r\n } catch {\r\n showHintOnce(\r\n \"tiny-segmenter\",\r\n \"install tiny-segmenter to improve Japanese search; falling back to whole-string tokenization\",\r\n );\r\n return null;\r\n }\r\n}\r\n\r\nfunction cleanTokens(tokens: string[]): string[] {\r\n const out: string[] = [];\r\n for (const t of tokens) {\r\n const trimmed = t.trim();\r\n if (trimmed) out.push(trimmed);\r\n }\r\n return out;\r\n}\r\n\r\nfunction segmentHan(text: string): string[] {\r\n const j = getJieba();\r\n if (!j) return [text];\r\n try {\r\n return cleanTokens(j.cut(text, true));\r\n } catch {\r\n return [text];\r\n }\r\n}\r\n\r\nfunction segmentKana(text: string): string[] {\r\n const s = getJaSegmenter();\r\n if (!s) return [text];\r\n try {\r\n return cleanTokens(s.segment(text));\r\n } catch {\r\n return [text];\r\n }\r\n}\r\n\r\nfunction segmentHangul(text: string): string[] {\r\n const out: string[] = [];\r\n for (const m of text.matchAll(HANGUL_BLOCK_RE)) {\r\n if (m[0]) out.push(m[0]);\r\n }\r\n return out;\r\n}\r\n\r\nexport function segmentCjk(text: string): string[] {\r\n if (!hasCjk(text)) return [text];\r\n\r\n const out: string[] = [];\r\n let cursor = 0;\r\n\r\n for (const m of text.matchAll(CJK_RUN_RE)) {\r\n const start = m.index ?? 0;\r\n const run = m[0];\r\n const end = start + run.length;\r\n\r\n if (start > cursor) {\r\n const piece = text.slice(cursor, start).trim();\r\n if (piece) out.push(piece);\r\n }\r\n\r\n if (HANGUL_RE.test(run)) {\r\n out.push(...segmentHangul(run));\r\n } else if (KANA_RE.test(run)) {\r\n out.push(...segmentKana(run));\r\n } else {\r\n out.push(...segmentHan(run));\r\n }\r\n\r\n cursor = end;\r\n }\r\n\r\n if (cursor < text.length) {\r\n const trailing = text.slice(cursor).trim();\r\n if (trailing) out.push(trailing);\r\n }\r\n\r\n return out;\r\n}\r\n\r\nexport function __resetCjkSegmenterStateForTests(): void {\r\n hintShown.clear();\r\n jiebaInstance = null;\r\n jiebaLoaded = false;\r\n jaSegmenterInstance = null;\r\n jaSegmenterLoaded = false;\r\n}\r\n","import { createHash } from \"node:crypto\";\r\nimport { hasCjk, segmentCjk } from \"./cjk-segmenter.js\";\r\n\r\nexport const KV = {\r\n sessions: \"mem:sessions\",\r\n observations: (sessionId: string) => `mem:obs:${sessionId}`,\r\n memories: \"mem:memories\",\r\n summaries: \"mem:summaries\",\r\n config: \"mem:config\",\r\n metrics: \"mem:metrics\",\r\n health: \"mem:health\",\r\n embeddings: (obsId: string) => `mem:emb:${obsId}`,\r\n bm25Index: \"mem:index:bm25\",\r\n relations: \"mem:relations\",\r\n profiles: \"mem:profiles\",\r\n claudeBridge: \"mem:claude-bridge\",\r\n graphNodes: \"mem:graph:nodes\",\r\n graphEdges: \"mem:graph:edges\",\r\n // #814: precomputed snapshot of the top-degree subgraph and aggregate\r\n // type counts. Saves /graph/query and /graph/stats from a full\r\n // kv.list enumeration over 75K+ node corpora, which exceeds the iii\r\n // invocation timeout and surfaces as \"Invocation stopped\" 500s.\r\n // Single fixed key (\"current\") so writes are read-modify-write under\r\n // the same keyed mutex as graph-extract.\r\n graphSnapshot: \"mem:graph:snapshot\",\r\n // #814 v2: targeted-lookup indexes so graph-extract never enumerates\r\n // the full nodes/edges scope. Each entry is a single small kv.get,\r\n // bounded payload — works at 75K+ nodes where kv.list would block\r\n // the worker event loop (37MB WS frame parse blocks heartbeat,\r\n // worker is declared dead before any Promise.race timer can fire).\r\n // - graphNameIndex: key `${type}|${name}` -> nodeId. Replaces the\r\n // existingNodes.find() O(n) dedup scan inside mem::graph-extract.\r\n // - graphEdgeKey: key `${src}|${tgt}|${type}` -> edgeId. Same for\r\n // edge dedup.\r\n // - graphNodeDegree: key nodeId -> incident-edge count. Read /\r\n // incremented on edge writes to maintain the snapshot top-N\r\n // ranking without scanning all edges.\r\n graphNameIndex: \"mem:graph:name-index\",\r\n graphEdgeKey: \"mem:graph:edge-key\",\r\n graphNodeDegree: \"mem:graph:node-degree\",\r\n semantic: \"mem:semantic\",\r\n procedural: \"mem:procedural\",\r\n teamShared: (teamId: string) => `mem:team:${teamId}:shared`,\r\n teamUsers: (teamId: string, userId: string) =>\r\n `mem:team:${teamId}:users:${userId}`,\r\n teamProfile: (teamId: string) => `mem:team:${teamId}:profile`,\r\n audit: \"mem:audit\",\r\n actions: \"mem:actions\",\r\n actionEdges: \"mem:action-edges\",\r\n leases: \"mem:leases\",\r\n routines: \"mem:routines\",\r\n routineRuns: \"mem:routine-runs\",\r\n signals: \"mem:signals\",\r\n checkpoints: \"mem:checkpoints\",\r\n mesh: \"mem:mesh\",\r\n sketches: \"mem:sketches\",\r\n facets: \"mem:facets\",\r\n sentinels: \"mem:sentinels\",\r\n crystals: \"mem:crystals\",\r\n lessons: \"mem:lessons\",\r\n insights: \"mem:insights\",\r\n graphEdgeHistory: \"mem:graph:edge-history\",\r\n enrichedChunks: (sessionId: string) => `mem:enriched:${sessionId}`,\r\n latentEmbeddings: (obsId: string) => `mem:latent:${obsId}`,\r\n retentionScores: \"mem:retention\",\r\n accessLog: \"mem:access\",\r\n imageRefs: \"mem:image-refs\",\r\n imageEmbeddings: \"mem:image-embeddings\",\r\n slots: \"mem:slots\",\r\n globalSlots: \"mem:slots:global\",\r\n state: \"mem:state\",\r\n commits: \"mem:commits\",\r\n // #771: tracks the most recent smart-search call per session, used by\r\n // the followup-rate diagnostic. Key = sessionId. TTL-swept hourly.\r\n recentSearches: \"mem:recent-searches\",\r\n} as const;\r\n\r\nexport const STREAM = {\r\n name: \"mem-live\",\r\n group: (sessionId: string) => sessionId,\r\n viewerGroup: \"viewer\",\r\n} as const;\r\n\r\nexport function generateId(prefix: string): string {\r\n const ts = Date.now().toString(36);\r\n const rand = crypto.randomUUID().replace(/-/g, \"\").slice(0, 12);\r\n return `${prefix}_${ts}_${rand}`;\r\n}\r\n\r\nexport function fingerprintId(prefix: string, content: string): string {\r\n const hash = createHash(\"sha256\").update(content).digest(\"hex\");\r\n return `${prefix}_${hash.slice(0, 16)}`;\r\n}\r\n\r\n/**\r\n * CJK-aware tokenizer for Jaccard similarity.\r\n *\r\n * For English/whitespace-delimited text we keep the original behaviour:\r\n * split on whitespace, filter tokens longer than 2 characters.\r\n *\r\n * For CJK text (Chinese/Japanese/Korean) whitespace splitting produces\r\n * near-disjoint sets because CJK text is typically written without spaces.\r\n * We reuse the existing `segmentCjk` function and additionally emit\r\n * character bigrams from CJK runs so that no-space Chinese text still\r\n * produces useful overlap.\r\n */\r\nfunction tokenizeForJaccard(text: string): Set<string> {\r\n // Fast path: no CJK characters → original whitespace split\r\n if (!hasCjk(text)) {\r\n return new Set(text.split(/\\s+/).filter((t) => t.length > 2));\r\n }\r\n\r\n // CJK path: segment + add character bigrams for overlap\r\n const segments = segmentCjk(text);\r\n const tokens = new Set<string>();\r\n\r\n for (const seg of segments) {\r\n // Keep the whole segment as a token (works for word-segmented Chinese\r\n // via jieba, and for English fragments in mixed text).\r\n const cleaned = seg.replace(/[^\\p{L}\\p{N}]/gu, \"\").toLowerCase();\r\n if (cleaned.length > 2) {\r\n tokens.add(cleaned);\r\n }\r\n\r\n // Add CJK character bigrams from each segment so unsegmented CJK\r\n // text can still produce meaningful overlap.\r\n const cjkChars = seg.match(/[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]/gu);\r\n if (cjkChars && cjkChars.length >= 2) {\r\n for (let i = 0; i < cjkChars.length - 1; i++) {\r\n tokens.add(cjkChars[i] + cjkChars[i + 1]);\r\n }\r\n }\r\n }\r\n\r\n return tokens;\r\n}\r\n\r\nexport function jaccardSimilarity(a: string, b: string): number {\r\n const setA = tokenizeForJaccard(a);\r\n const setB = tokenizeForJaccard(b);\r\n if (setA.size === 0 && setB.size === 0) return 1;\r\n if (setA.size === 0 || setB.size === 0) return 0;\r\n let intersection = 0;\r\n for (const word of setA) {\r\n if (setB.has(word)) intersection++;\r\n }\r\n return intersection / (setA.size + setB.size - intersection);\r\n}\r\n"],"mappings":";;;AAEA,MAAM,aAAa,cAAc,OAAO,KAAK,IAAI;AAEjD,MAAM,SAAS;AAEf,MAAM,UAAU;AAChB,MAAM,YAAY;AAClB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AAIxB,MAAM,4BAAY,IAAI,KAAa;AAEnC,SAAgB,OAAO,MAAuB;AAC5C,QAAO,OAAO,KAAK,KAAK;;AAU1B,SAAS,aAAa,KAAa,SAAuB;AACxD,KAAI,UAAU,IAAI,IAAI,CAAE;AACxB,WAAU,IAAI,IAAI;AAClB,KAAI,OAAO,YAAY,eAAe,QAAQ,QAAQ,MACpD,SAAQ,OAAO,MAAM,gBAAgB,QAAQ,IAAI;;AAQrD,IAAI,gBAAsC;AAC1C,IAAI,cAAc;AAElB,SAAS,WAAiC;AACxC,KAAI,YAAa,QAAO;AACxB,eAAc;AACd,KAAI;EACF,MAAM,MAAM,WAAW,iBAAiB;AAMxC,MAAI;GACF,MAAM,UAAU,WAAW,sBAAsB;AACjD,mBAAgB,IAAI,MAAM,SAAS,QAAQ,KAAK;UAC1C;AACN,mBAAgB,IAAI,IAAI,OAAO;;AAEjC,SAAO;SACD;AACN,eACE,SACA,8FACD;AACD,SAAO;;;AAQX,IAAI,sBAA0C;AAC9C,IAAI,oBAAoB;AAExB,SAAS,iBAAqC;AAC5C,KAAI,kBAAmB,QAAO;AAC9B,qBAAoB;AACpB,KAAI;AAEF,wBAAsB,KADT,WAAW,iBACM,GAAE;AAChC,SAAO;SACD;AACN,eACE,kBACA,+FACD;AACD,SAAO;;;AAIX,SAAS,YAAY,QAA4B;CAC/C,MAAM,MAAgB,EAAE;AACxB,MAAK,MAAM,KAAK,QAAQ;EACtB,MAAM,UAAU,EAAE,MAAM;AACxB,MAAI,QAAS,KAAI,KAAK,QAAQ;;AAEhC,QAAO;;AAGT,SAAS,WAAW,MAAwB;CAC1C,MAAM,IAAI,UAAU;AACpB,KAAI,CAAC,EAAG,QAAO,CAAC,KAAK;AACrB,KAAI;AACF,SAAO,YAAY,EAAE,IAAI,MAAM,KAAK,CAAC;SAC/B;AACN,SAAO,CAAC,KAAK;;;AAIjB,SAAS,YAAY,MAAwB;CAC3C,MAAM,IAAI,gBAAgB;AAC1B,KAAI,CAAC,EAAG,QAAO,CAAC,KAAK;AACrB,KAAI;AACF,SAAO,YAAY,EAAE,QAAQ,KAAK,CAAC;SAC7B;AACN,SAAO,CAAC,KAAK;;;AAIjB,SAAS,cAAc,MAAwB;CAC7C,MAAM,MAAgB,EAAE;AACxB,MAAK,MAAM,KAAK,KAAK,SAAS,gBAAgB,CAC5C,KAAI,EAAE,GAAI,KAAI,KAAK,EAAE,GAAG;AAE1B,QAAO;;AAGT,SAAgB,WAAW,MAAwB;AACjD,KAAI,CAAC,OAAO,KAAK,CAAE,QAAO,CAAC,KAAK;CAEhC,MAAM,MAAgB,EAAE;CACxB,IAAI,SAAS;AAEb,MAAK,MAAM,KAAK,KAAK,SAAS,WAAW,EAAE;EACzC,MAAM,QAAQ,EAAE,SAAS;EACzB,MAAM,MAAM,EAAE;EACd,MAAM,MAAM,QAAQ,IAAI;AAExB,MAAI,QAAQ,QAAQ;GAClB,MAAM,QAAQ,KAAK,MAAM,QAAQ,MAAM,CAAC,MAAM;AAC9C,OAAI,MAAO,KAAI,KAAK,MAAM;;AAG5B,MAAI,UAAU,KAAK,IAAI,CACrB,KAAI,KAAK,GAAG,cAAc,IAAI,CAAC;WACtB,QAAQ,KAAK,IAAI,CAC1B,KAAI,KAAK,GAAG,YAAY,IAAI,CAAC;MAE7B,KAAI,KAAK,GAAG,WAAW,IAAI,CAAC;AAG9B,WAAS;;AAGX,KAAI,SAAS,KAAK,QAAQ;EACxB,MAAM,WAAW,KAAK,MAAM,OAAO,CAAC,MAAM;AAC1C,MAAI,SAAU,KAAI,KAAK,SAAS;;AAGlC,QAAO;;;;AC5JT,MAAa,KAAK;CAChB,UAAU;CACV,eAAe,cAAsB,WAAW;CAChD,UAAU;CACV,WAAW;CACX,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,aAAa,UAAkB,WAAW;CAC1C,WAAW;CACX,WAAW;CACX,UAAU;CACV,cAAc;CACd,YAAY;CACZ,YAAY;CAOZ,eAAe;CAaf,gBAAgB;CAChB,cAAc;CACd,iBAAiB;CACjB,UAAU;CACV,YAAY;CACZ,aAAa,WAAmB,YAAY,OAAO;CACnD,YAAY,QAAgB,WAC1B,YAAY,OAAO,SAAS;CAC9B,cAAc,WAAmB,YAAY,OAAO;CACpD,OAAO;CACP,SAAS;CACT,aAAa;CACb,QAAQ;CACR,UAAU;CACV,aAAa;CACb,SAAS;CACT,aAAa;CACb,MAAM;CACN,UAAU;CACV,QAAQ;CACR,WAAW;CACX,UAAU;CACV,SAAS;CACT,UAAU;CACV,kBAAkB;CAClB,iBAAiB,cAAsB,gBAAgB;CACvD,mBAAmB,UAAkB,cAAc;CACnD,iBAAiB;CACjB,WAAW;CACX,WAAW;CACX,iBAAiB;CACjB,OAAO;CACP,aAAa;CACb,OAAO;CACP,SAAS;CAGT,gBAAgB;CACjB;AAED,MAAa,SAAS;CACpB,MAAM;CACN,QAAQ,cAAsB;CAC9B,aAAa;CACd;AAED,SAAgB,WAAW,QAAwB;AAGjD,QAAO,GAAG,OAAO,GAFN,KAAK,KAAK,CAAC,SAAS,GAET,CAAC,GADV,OAAO,YAAY,CAAC,QAAQ,MAAM,GAAG,CAAC,MAAM,GAAG,GAC9B;;AAGhC,SAAgB,cAAc,QAAgB,SAAyB;AAErE,QAAO,GAAG,OAAO,GADJ,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MACjC,CAAC,MAAM,GAAG,GAAG;;;;;;;;;;;;;;AAevC,SAAS,mBAAmB,MAA2B;AAErD,KAAI,CAAC,OAAO,KAAK,CACf,QAAO,IAAI,IAAI,KAAK,MAAM,MAAM,CAAC,QAAQ,MAAM,EAAE,SAAS,EAAE,CAAC;CAI/D,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,yBAAS,IAAI,KAAa;AAEhC,MAAK,MAAM,OAAO,UAAU;EAG1B,MAAM,UAAU,IAAI,QAAQ,mBAAmB,GAAG,CAAC,aAAa;AAChE,MAAI,QAAQ,SAAS,EACnB,QAAO,IAAI,QAAQ;EAKrB,MAAM,WAAW,IAAI,MAAM,4EAA4E;AACvG,MAAI,YAAY,SAAS,UAAU,EACjC,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,IACvC,QAAO,IAAI,SAAS,KAAK,SAAS,IAAI,GAAG;;AAK/C,QAAO;;AAGT,SAAgB,kBAAkB,GAAW,GAAmB;CAC9D,MAAM,OAAO,mBAAmB,EAAE;CAClC,MAAM,OAAO,mBAAmB,EAAE;AAClC,KAAI,KAAK,SAAS,KAAK,KAAK,SAAS,EAAG,QAAO;AAC/C,KAAI,KAAK,SAAS,KAAK,KAAK,SAAS,EAAG,QAAO;CAC/C,IAAI,eAAe;AACnB,MAAK,MAAM,QAAQ,KACjB,KAAI,KAAK,IAAI,KAAK,CAAE;AAEtB,QAAO,gBAAgB,KAAK,OAAO,KAAK,OAAO"}
@@ -1,9 +1,8 @@
1
- import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./schema-CNiCWzV9.mjs";
1
+ import { a as jaccardSimilarity, i as generateId, n as STREAM, o as hasCjk, r as fingerprintId, s as segmentCjk, t as KV } from "./schema-BQLY6d8C.mjs";
2
2
  import { n as logger, t as bootLog } from "./logger-BEB7pCI9.mjs";
3
3
  import { i as VERSION, n as getAllTools, r as getVisibleTools } from "./tools-registry-DCt2KAWA.mjs";
4
4
  import { a as isManagedImagePath, i as getMaxBytes, n as IMAGES_DIR, o as saveImageToDisk, r as deleteImage, t as withKeyedLock } from "./keyed-mutex-DTHtcZrt.mjs";
5
5
  import { _ as loadFallbackConfig, a as getEnvVar, c as isAgentScopeIsolated, d as isContextInjectionEnabled, f as isDropStaleIndexEnabled, g as loadEmbeddingConfig, h as loadConfig, i as getConsolidationDecayDays, l as isAutoCompressEnabled, m as loadClaudeBridgeConfig, n as detectLlmProviderKind, o as getFollowupWindowSeconds, p as isGraphExtractionEnabled, r as getAgentId, t as detectEmbeddingProvider, u as isConsolidationEnabled, v as loadSnapshotConfig, y as loadTeamConfig } from "./config-ChC9EtgV.mjs";
6
- import { createRequire } from "node:module";
7
6
  import { execFile } from "node:child_process";
8
7
  import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
9
8
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
@@ -609,25 +608,25 @@ function l2Normalize(vec) {
609
608
  }
610
609
  //#endregion
611
610
  //#region src/providers/embedding/openai.ts
612
- const DEFAULT_MODEL$1 = "text-embedding-3-small";
611
+ const DEFAULT_MODEL$2 = "text-embedding-3-small";
613
612
  /**
614
613
  * Known OpenAI embedding model dimensions. Extend as new models ship.
615
614
  * Override in any case via OPENAI_EMBEDDING_DIMENSIONS for custom or
616
615
  * self-hosted OpenAI-compatible endpoints returning non-standard sizes.
617
616
  */
618
- const MODEL_DIMENSIONS = {
617
+ const MODEL_DIMENSIONS$1 = {
619
618
  "text-embedding-3-small": 1536,
620
619
  "text-embedding-3-large": 3072,
621
620
  "text-embedding-ada-002": 1536
622
621
  };
623
- const DEFAULT_DIMENSIONS = MODEL_DIMENSIONS[DEFAULT_MODEL$1] ?? 1536;
624
- function resolveDimensions(model, override) {
622
+ const DEFAULT_DIMENSIONS$1 = MODEL_DIMENSIONS$1[DEFAULT_MODEL$2] ?? 1536;
623
+ function resolveDimensions$1(model, override) {
625
624
  if (override !== void 0 && override.trim().length > 0) {
626
625
  const parsed = parseInt(override, 10);
627
626
  if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`OPENAI_EMBEDDING_DIMENSIONS must be a positive integer, got: ${override}`);
628
627
  return parsed;
629
628
  }
630
- return MODEL_DIMENSIONS[model] ?? DEFAULT_DIMENSIONS;
629
+ return MODEL_DIMENSIONS$1[model] ?? DEFAULT_DIMENSIONS$1;
631
630
  }
632
631
  /**
633
632
  * OpenAI-compatible embedding provider.
@@ -677,8 +676,8 @@ var OpenAIEmbeddingProvider = class {
677
676
  this.apiKey = apiKey || getEnvVar("OPENAI_EMBEDDING_API_KEY") || getEnvVar("OPENAI_API_KEY") || "";
678
677
  if (!this.apiKey) throw new Error("API key is required (via constructor, OPENAI_EMBEDDING_API_KEY, or OPENAI_API_KEY)");
679
678
  this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_EMBEDDING_BASE_URL") || getEnvVar("OPENAI_BASE_URL"));
680
- this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
681
- this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
679
+ this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$2;
680
+ this.dimensions = resolveDimensions$1(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
682
681
  this.isAzure = detectAzure(this.baseUrl);
683
682
  this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || "2024-08-01-preview";
684
683
  }
@@ -775,15 +774,36 @@ var CohereEmbeddingProvider = class {
775
774
  //#endregion
776
775
  //#region src/providers/embedding/openrouter.ts
777
776
  const API_URL = "https://openrouter.ai/api/v1/embeddings";
777
+ /**
778
+ * Known OpenRouter embedding model dimensions. Extend as new models ship.
779
+ * Override via OPENROUTER_EMBEDDING_DIMENSIONS for models not listed here
780
+ * or custom OpenAI-compatible endpoints returning non-standard sizes.
781
+ */
782
+ const MODEL_DIMENSIONS = {
783
+ "openai/text-embedding-3-small": 1536,
784
+ "openai/text-embedding-3-large": 3072,
785
+ "openai/text-embedding-ada-002": 1536
786
+ };
787
+ const DEFAULT_MODEL$1 = "openai/text-embedding-3-small";
788
+ const DEFAULT_DIMENSIONS = MODEL_DIMENSIONS[DEFAULT_MODEL$1] ?? 1536;
789
+ function resolveDimensions(model, override) {
790
+ if (override !== void 0 && override.trim().length > 0) {
791
+ const parsed = parseInt(override, 10);
792
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`OPENROUTER_EMBEDDING_DIMENSIONS must be a positive integer, got: ${override}`);
793
+ return parsed;
794
+ }
795
+ return MODEL_DIMENSIONS[model] ?? DEFAULT_DIMENSIONS;
796
+ }
778
797
  var OpenRouterEmbeddingProvider = class {
779
798
  name = "openrouter";
780
- dimensions = 1536;
799
+ dimensions;
781
800
  apiKey;
782
801
  model;
783
802
  constructor(apiKey) {
784
803
  this.apiKey = apiKey || getEnvVar("OPENROUTER_API_KEY") || "";
785
804
  if (!this.apiKey) throw new Error("OPENROUTER_API_KEY is required");
786
- this.model = getEnvVar("OPENROUTER_EMBEDDING_MODEL") || "openai/text-embedding-3-small";
805
+ this.model = getEnvVar("OPENROUTER_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
806
+ this.dimensions = resolveDimensions(this.model, getEnvVar("OPENROUTER_EMBEDDING_DIMENSIONS"));
787
807
  }
788
808
  async embed(text) {
789
809
  const [result] = await this.embedBatch([text]);
@@ -2072,109 +2092,6 @@ function getSynonyms(stemmedTerm) {
2072
2092
  return syns ? [...syns] : [];
2073
2093
  }
2074
2094
  //#endregion
2075
- //#region src/state/cjk-segmenter.ts
2076
- const cjkRequire = createRequire(import.meta.url);
2077
- const CJK_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
2078
- const KANA_RE = /[\p{Script=Hiragana}\p{Script=Katakana}]/u;
2079
- const HANGUL_RE = /\p{Script=Hangul}/u;
2080
- const CJK_RUN_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
2081
- const HANGUL_BLOCK_RE = /[가-힯]+/g;
2082
- const hintShown = /* @__PURE__ */ new Set();
2083
- function hasCjk(text) {
2084
- return CJK_RE.test(text);
2085
- }
2086
- function showHintOnce(key, message) {
2087
- if (hintShown.has(key)) return;
2088
- hintShown.add(key);
2089
- if (typeof process !== "undefined" && process.stderr?.write) process.stderr.write(`agentmemory: ${message}\n`);
2090
- }
2091
- let jiebaInstance = null;
2092
- let jiebaLoaded = false;
2093
- function getJieba() {
2094
- if (jiebaLoaded) return jiebaInstance;
2095
- jiebaLoaded = true;
2096
- try {
2097
- const mod = cjkRequire("@node-rs/jieba");
2098
- try {
2099
- const dictMod = cjkRequire("@node-rs/jieba/dict");
2100
- jiebaInstance = mod.Jieba.withDict(dictMod.dict);
2101
- } catch {
2102
- jiebaInstance = new mod.Jieba();
2103
- }
2104
- return jiebaInstance;
2105
- } catch {
2106
- showHintOnce("jieba", "install @node-rs/jieba to improve Chinese search; falling back to whole-string tokenization");
2107
- return null;
2108
- }
2109
- }
2110
- let jaSegmenterInstance = null;
2111
- let jaSegmenterLoaded = false;
2112
- function getJaSegmenter() {
2113
- if (jaSegmenterLoaded) return jaSegmenterInstance;
2114
- jaSegmenterLoaded = true;
2115
- try {
2116
- jaSegmenterInstance = new (cjkRequire("tiny-segmenter"))();
2117
- return jaSegmenterInstance;
2118
- } catch {
2119
- showHintOnce("tiny-segmenter", "install tiny-segmenter to improve Japanese search; falling back to whole-string tokenization");
2120
- return null;
2121
- }
2122
- }
2123
- function cleanTokens(tokens) {
2124
- const out = [];
2125
- for (const t of tokens) {
2126
- const trimmed = t.trim();
2127
- if (trimmed) out.push(trimmed);
2128
- }
2129
- return out;
2130
- }
2131
- function segmentHan(text) {
2132
- const j = getJieba();
2133
- if (!j) return [text];
2134
- try {
2135
- return cleanTokens(j.cut(text, true));
2136
- } catch {
2137
- return [text];
2138
- }
2139
- }
2140
- function segmentKana(text) {
2141
- const s = getJaSegmenter();
2142
- if (!s) return [text];
2143
- try {
2144
- return cleanTokens(s.segment(text));
2145
- } catch {
2146
- return [text];
2147
- }
2148
- }
2149
- function segmentHangul(text) {
2150
- const out = [];
2151
- for (const m of text.matchAll(HANGUL_BLOCK_RE)) if (m[0]) out.push(m[0]);
2152
- return out;
2153
- }
2154
- function segmentCjk(text) {
2155
- if (!hasCjk(text)) return [text];
2156
- const out = [];
2157
- let cursor = 0;
2158
- for (const m of text.matchAll(CJK_RUN_RE)) {
2159
- const start = m.index ?? 0;
2160
- const run = m[0];
2161
- const end = start + run.length;
2162
- if (start > cursor) {
2163
- const piece = text.slice(cursor, start).trim();
2164
- if (piece) out.push(piece);
2165
- }
2166
- if (HANGUL_RE.test(run)) out.push(...segmentHangul(run));
2167
- else if (KANA_RE.test(run)) out.push(...segmentKana(run));
2168
- else out.push(...segmentHan(run));
2169
- cursor = end;
2170
- }
2171
- if (cursor < text.length) {
2172
- const trailing = text.slice(cursor).trim();
2173
- if (trailing) out.push(trailing);
2174
- }
2175
- return out;
2176
- }
2177
- //#endregion
2178
2095
  //#region src/state/search-index.ts
2179
2096
  var SearchIndex = class SearchIndex {
2180
2097
  entries = /* @__PURE__ */ new Map();
@@ -3338,7 +3255,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3338
3255
  if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
3339
3256
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
3340
3257
  raw.imageData = filePath;
3341
- const { incrementImageRef } = await import("./image-refs-C2YU7BIm.mjs");
3258
+ const { incrementImageRef } = await import("./image-refs-Cc7-ua5j.mjs");
3342
3259
  await incrementImageRef(kv, filePath);
3343
3260
  sdk.trigger({
3344
3261
  function_id: "mem::disk-size-delta",
@@ -3359,7 +3276,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3359
3276
  await kv.set(KV.observations(payload.sessionId), obsId, raw);
3360
3277
  } catch (error) {
3361
3278
  if (raw.imageData) try {
3362
- const { decrementImageRef } = await import("./image-refs-C2YU7BIm.mjs");
3279
+ const { decrementImageRef } = await import("./image-refs-Cc7-ua5j.mjs");
3363
3280
  await decrementImageRef(kv, sdk, raw.imageData);
3364
3281
  } catch (rollbackError) {
3365
3282
  logger.error("Failed to roll back image ref after observation write failure", {
@@ -3533,7 +3450,7 @@ function registerImageQuotaCleanup(sdk, kv) {
3533
3450
  await withKeyedLock(`imgRef:${f.filePath}`, async () => {
3534
3451
  let refCount;
3535
3452
  try {
3536
- const { getImageRefCount } = await import("./image-refs-C2YU7BIm.mjs");
3453
+ const { getImageRefCount } = await import("./image-refs-Cc7-ua5j.mjs");
3537
3454
  refCount = await getImageRefCount(kv, f.filePath);
3538
3455
  } catch (err) {
3539
3456
  logger.error("Failed to read refCount; skipping eviction", {
@@ -4945,7 +4862,7 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
4945
4862
  error: "no_observations"
4946
4863
  };
4947
4864
  }
4948
- if (provider.name === "noop") {
4865
+ if (provider.name === "noop" || provider.name === "resilient(noop)") {
4949
4866
  logger.info("Summarize skipped — no LLM provider configured", { sessionId });
4950
4867
  return {
4951
4868
  success: false,
@@ -5556,6 +5473,34 @@ function registerPatternsFunction(sdk, kv) {
5556
5473
  });
5557
5474
  }
5558
5475
  //#endregion
5476
+ //#region src/state/memory-dedup.ts
5477
+ /**
5478
+ * Negation guard for memory dedup.
5479
+ *
5480
+ * Prevents "do X" from superseding "do not X" (and vice versa) even when
5481
+ * the two strings share enough tokens to exceed the Jaccard threshold.
5482
+ *
5483
+ * The guard is deliberately narrow and conservative:
5484
+ * - Only triggers when one side has a negation marker and the other does not.
5485
+ * - Supports English ("not", "never", "don't", "do not") and CJK
5486
+ * ("不要", "别", "無", "なし", "않") markers.
5487
+ * - If BOTH sides have negation markers, they are treated as compatible
5488
+ * (both say "don't do X") and the guard does not fire.
5489
+ */
5490
+ const EN_NEGATION = /\b(not|never|don't|do not|cannot|should not|must not|no)\b/i;
5491
+ const CJK_NEGATION = /不要|别|勿|無|无|不|なし|않|안|못/i;
5492
+ function hasNegation(text) {
5493
+ return EN_NEGATION.test(text) || CJK_NEGATION.test(text);
5494
+ }
5495
+ /**
5496
+ * Returns true if the two texts appear to be in a negation conflict
5497
+ * (one says do X, the other says do NOT do X) and should NOT be superseded.
5498
+ */
5499
+ function isNegationConflict(a, b) {
5500
+ if (hasNegation(a) === hasNegation(b)) return false;
5501
+ return true;
5502
+ }
5503
+ //#endregion
5559
5504
  //#region src/functions/remember.ts
5560
5505
  function registerRememberFunction(sdk, kv) {
5561
5506
  sdk.registerFunction("mem::remember", async (data) => {
@@ -5594,7 +5539,7 @@ function registerRememberFunction(sdk, kv) {
5594
5539
  for (const existing of existingMemories) {
5595
5540
  if (existing.isLatest === false) continue;
5596
5541
  if (project && existing.project && existing.project !== project) continue;
5597
- if (jaccardSimilarity(lowerContent, existing.content.toLowerCase()) > .7) {
5542
+ if (jaccardSimilarity(lowerContent, existing.content.toLowerCase()) > .7 && !isNegationConflict(lowerContent, existing.content.toLowerCase())) {
5598
5543
  supersededId = existing.id;
5599
5544
  supersededVersion = existing.version ?? 1;
5600
5545
  supersededMemory = existing;
@@ -5744,7 +5689,7 @@ function registerRememberFunction(sdk, kv) {
5744
5689
  const deletedMemoryIds = [];
5745
5690
  const deletedObservationIds = [];
5746
5691
  let deletedSession = false;
5747
- const { decrementImageRef } = await import("./image-refs-C2YU7BIm.mjs");
5692
+ const { decrementImageRef } = await import("./image-refs-Cc7-ua5j.mjs");
5748
5693
  if (data.memoryId) {
5749
5694
  const mem = await kv.get(KV.memories, data.memoryId);
5750
5695
  await kv.delete(KV.memories, data.memoryId);
@@ -5851,7 +5796,7 @@ async function runRecoveredSessionConsolidation(sdk) {
5851
5796
  function registerEvictFunction(sdk, kv) {
5852
5797
  sdk.registerFunction("mem::evict", async (data) => {
5853
5798
  const dryRun = data?.dryRun ?? false;
5854
- const { decrementImageRef } = await import("./image-refs-C2YU7BIm.mjs");
5799
+ const { decrementImageRef } = await import("./image-refs-Cc7-ua5j.mjs");
5855
5800
  const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
5856
5801
  const cfg = {
5857
5802
  ...DEFAULTS$1,
@@ -6666,7 +6611,7 @@ function registerAutoForgetFunction(sdk, kv) {
6666
6611
  sdk.registerFunction("mem::auto-forget", async (data) => {
6667
6612
  const dryRun = data?.dryRun ?? false;
6668
6613
  const now = Date.now();
6669
- const { decrementImageRef } = await import("./image-refs-C2YU7BIm.mjs");
6614
+ const { decrementImageRef } = await import("./image-refs-Cc7-ua5j.mjs");
6670
6615
  const result = {
6671
6616
  ttlExpired: [],
6672
6617
  contradictions: [],
@@ -13804,7 +13749,7 @@ function registerRetentionFunctions(sdk, kv) {
13804
13749
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
13805
13750
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
13806
13751
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
13807
- const { decrementImageRef } = await import("./image-refs-C2YU7BIm.mjs");
13752
+ const { decrementImageRef } = await import("./image-refs-Cc7-ua5j.mjs");
13808
13753
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
13809
13754
  if (data?.dryRun) return {
13810
13755
  success: true,
@@ -15017,6 +14962,10 @@ function parseOptionalInt(raw) {
15017
14962
  const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
15018
14963
  return Number.isFinite(n) ? n : void 0;
15019
14964
  }
14965
+ function parsePositiveLimit(raw) {
14966
+ const n = parseOptionalInt(raw);
14967
+ return n !== void 0 && n > 0 ? n : void 0;
14968
+ }
15020
14969
  function checkAuth(req, secret) {
15021
14970
  if (!secret) return null;
15022
14971
  const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
@@ -15405,11 +15354,12 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15405
15354
  if (authErr) return authErr;
15406
15355
  const sessions = await kv.list(KV.sessions);
15407
15356
  sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
15357
+ const limit = parsePositiveLimit(req.query_params?.["limit"]);
15408
15358
  return {
15409
15359
  status_code: 200,
15410
15360
  body: {
15411
15361
  success: true,
15412
- sessions
15362
+ sessions: limit !== void 0 ? sessions.slice(0, limit) : sessions
15413
15363
  }
15414
15364
  };
15415
15365
  });
@@ -15681,10 +15631,12 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15681
15631
  const wildcardAgent = normalizedAgentId === "*";
15682
15632
  const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
15683
15633
  const filtered = filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions;
15684
- const summaries = await Promise.all(filtered.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
15634
+ const limit = parsePositiveLimit(req.query_params?.["limit"]);
15635
+ const sliced = limit !== void 0 ? filtered.slice(0, limit) : filtered;
15636
+ const summaries = await Promise.all(sliced.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
15685
15637
  return {
15686
15638
  status_code: 200,
15687
- body: { sessions: filtered.map((s, i) => summaries[i] ? {
15639
+ body: { sessions: sliced.map((s, i) => summaries[i] ? {
15688
15640
  ...s,
15689
15641
  summary: summaries[i]
15690
15642
  } : s) }
@@ -15881,13 +15833,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15881
15833
  sdk.registerFunction("api::consolidate", async (req) => {
15882
15834
  const authErr = checkAuth(req, secret);
15883
15835
  if (authErr) return authErr;
15884
- return {
15885
- status_code: 200,
15886
- body: await sdk.trigger({
15887
- function_id: "mem::consolidate",
15888
- payload: req.body
15889
- })
15890
- };
15836
+ try {
15837
+ return {
15838
+ status_code: 200,
15839
+ body: await sdk.trigger({
15840
+ function_id: "mem::consolidate",
15841
+ payload: req.body
15842
+ })
15843
+ };
15844
+ } catch (err) {
15845
+ const message = err instanceof Error ? err.message : String(err);
15846
+ logger.warn("api::consolidate failed", { error: message });
15847
+ return {
15848
+ status_code: 500,
15849
+ body: { error: message }
15850
+ };
15851
+ }
15891
15852
  });
15892
15853
  sdk.registerTrigger({
15893
15854
  type: "http",
@@ -20592,6 +20553,15 @@ async function main() {
20592
20553
  if (snapshotConfig.enabled) {
20593
20554
  registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
20594
20555
  bootLog(`Git snapshots: ${snapshotConfig.dir} (every ${snapshotConfig.interval}s)`);
20556
+ const snapshotIntervalMs = snapshotConfig.interval * 1e3;
20557
+ setInterval(async () => {
20558
+ try {
20559
+ await sdk.trigger({
20560
+ function_id: "mem::snapshot-create",
20561
+ payload: {}
20562
+ });
20563
+ } catch {}
20564
+ }, snapshotIntervalMs).unref();
20595
20565
  }
20596
20566
  const bm25Index = getSearchIndex();
20597
20567
  const graphWeight = parseFloat(getEnvVar("AGENTMEMORY_GRAPH_WEIGHT") || "0.3");
@@ -20742,4 +20712,4 @@ main().catch((err) => {
20742
20712
  //#endregion
20743
20713
  export {};
20744
20714
 
20745
- //# sourceMappingURL=src-Cr3pH_uH.mjs.map
20715
+ //# sourceMappingURL=src-Dl5_tJgD.mjs.map