@mnemoai/core 1.1.0 → 1.1.1

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.
Files changed (220) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +7 -0
  4. package/dist/cli.js.map +7 -0
  5. package/dist/index.d.ts +128 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/{index.ts → dist/index.js} +526 -1333
  8. package/dist/index.js.map +7 -0
  9. package/dist/src/access-tracker.d.ts +97 -0
  10. package/dist/src/access-tracker.d.ts.map +1 -0
  11. package/dist/src/access-tracker.js +184 -0
  12. package/dist/src/access-tracker.js.map +7 -0
  13. package/dist/src/adapters/chroma.d.ts +31 -0
  14. package/dist/src/adapters/chroma.d.ts.map +1 -0
  15. package/{src/adapters/chroma.ts → dist/src/adapters/chroma.js} +45 -107
  16. package/dist/src/adapters/chroma.js.map +7 -0
  17. package/dist/src/adapters/lancedb.d.ts +29 -0
  18. package/dist/src/adapters/lancedb.d.ts.map +1 -0
  19. package/{src/adapters/lancedb.ts → dist/src/adapters/lancedb.js} +41 -109
  20. package/dist/src/adapters/lancedb.js.map +7 -0
  21. package/dist/src/adapters/pgvector.d.ts +33 -0
  22. package/dist/src/adapters/pgvector.d.ts.map +1 -0
  23. package/{src/adapters/pgvector.ts → dist/src/adapters/pgvector.js} +42 -104
  24. package/dist/src/adapters/pgvector.js.map +7 -0
  25. package/dist/src/adapters/qdrant.d.ts +34 -0
  26. package/dist/src/adapters/qdrant.d.ts.map +1 -0
  27. package/dist/src/adapters/qdrant.js +132 -0
  28. package/dist/src/adapters/qdrant.js.map +7 -0
  29. package/dist/src/adaptive-retrieval.d.ts +14 -0
  30. package/dist/src/adaptive-retrieval.d.ts.map +1 -0
  31. package/dist/src/adaptive-retrieval.js +52 -0
  32. package/dist/src/adaptive-retrieval.js.map +7 -0
  33. package/dist/src/audit-log.d.ts +56 -0
  34. package/dist/src/audit-log.d.ts.map +1 -0
  35. package/dist/src/audit-log.js +139 -0
  36. package/dist/src/audit-log.js.map +7 -0
  37. package/dist/src/chunker.d.ts +45 -0
  38. package/dist/src/chunker.d.ts.map +1 -0
  39. package/dist/src/chunker.js +157 -0
  40. package/dist/src/chunker.js.map +7 -0
  41. package/dist/src/config.d.ts +70 -0
  42. package/dist/src/config.d.ts.map +1 -0
  43. package/dist/src/config.js +142 -0
  44. package/dist/src/config.js.map +7 -0
  45. package/dist/src/decay-engine.d.ts +73 -0
  46. package/dist/src/decay-engine.d.ts.map +1 -0
  47. package/dist/src/decay-engine.js +119 -0
  48. package/dist/src/decay-engine.js.map +7 -0
  49. package/dist/src/embedder.d.ts +94 -0
  50. package/dist/src/embedder.d.ts.map +1 -0
  51. package/{src/embedder.ts → dist/src/embedder.js} +119 -317
  52. package/dist/src/embedder.js.map +7 -0
  53. package/dist/src/extraction-prompts.d.ts +12 -0
  54. package/dist/src/extraction-prompts.d.ts.map +1 -0
  55. package/dist/src/extraction-prompts.js +311 -0
  56. package/dist/src/extraction-prompts.js.map +7 -0
  57. package/dist/src/license.d.ts +29 -0
  58. package/dist/src/license.d.ts.map +1 -0
  59. package/{src/license.ts → dist/src/license.js} +42 -113
  60. package/dist/src/license.js.map +7 -0
  61. package/dist/src/llm-client.d.ts +23 -0
  62. package/dist/src/llm-client.d.ts.map +1 -0
  63. package/{src/llm-client.ts → dist/src/llm-client.js} +22 -55
  64. package/dist/src/llm-client.js.map +7 -0
  65. package/dist/src/logger.d.ts +33 -0
  66. package/dist/src/logger.d.ts.map +1 -0
  67. package/dist/src/logger.js +35 -0
  68. package/dist/src/logger.js.map +7 -0
  69. package/dist/src/mcp-server.d.ts +16 -0
  70. package/dist/src/mcp-server.d.ts.map +1 -0
  71. package/{src/mcp-server.ts → dist/src/mcp-server.js} +81 -181
  72. package/dist/src/mcp-server.js.map +7 -0
  73. package/dist/src/memory-categories.d.ts +40 -0
  74. package/dist/src/memory-categories.d.ts.map +1 -0
  75. package/dist/src/memory-categories.js +33 -0
  76. package/dist/src/memory-categories.js.map +7 -0
  77. package/dist/src/memory-upgrader.d.ts +71 -0
  78. package/dist/src/memory-upgrader.d.ts.map +1 -0
  79. package/dist/src/memory-upgrader.js +238 -0
  80. package/dist/src/memory-upgrader.js.map +7 -0
  81. package/dist/src/migrate.d.ts +47 -0
  82. package/dist/src/migrate.d.ts.map +1 -0
  83. package/{src/migrate.ts → dist/src/migrate.js} +57 -165
  84. package/dist/src/migrate.js.map +7 -0
  85. package/dist/src/mnemo.d.ts +67 -0
  86. package/dist/src/mnemo.d.ts.map +1 -0
  87. package/dist/src/mnemo.js +66 -0
  88. package/dist/src/mnemo.js.map +7 -0
  89. package/dist/src/noise-filter.d.ts +23 -0
  90. package/dist/src/noise-filter.d.ts.map +1 -0
  91. package/dist/src/noise-filter.js +62 -0
  92. package/dist/src/noise-filter.js.map +7 -0
  93. package/dist/src/noise-prototypes.d.ts +40 -0
  94. package/dist/src/noise-prototypes.d.ts.map +1 -0
  95. package/dist/src/noise-prototypes.js +116 -0
  96. package/dist/src/noise-prototypes.js.map +7 -0
  97. package/dist/src/observability.d.ts +16 -0
  98. package/dist/src/observability.d.ts.map +1 -0
  99. package/dist/src/observability.js +53 -0
  100. package/dist/src/observability.js.map +7 -0
  101. package/dist/src/query-tracker.d.ts +27 -0
  102. package/dist/src/query-tracker.d.ts.map +1 -0
  103. package/dist/src/query-tracker.js +32 -0
  104. package/dist/src/query-tracker.js.map +7 -0
  105. package/dist/src/reflection-event-store.d.ts +44 -0
  106. package/dist/src/reflection-event-store.d.ts.map +1 -0
  107. package/dist/src/reflection-event-store.js +50 -0
  108. package/dist/src/reflection-event-store.js.map +7 -0
  109. package/dist/src/reflection-item-store.d.ts +58 -0
  110. package/dist/src/reflection-item-store.d.ts.map +1 -0
  111. package/dist/src/reflection-item-store.js +69 -0
  112. package/dist/src/reflection-item-store.js.map +7 -0
  113. package/dist/src/reflection-mapped-metadata.d.ts +47 -0
  114. package/dist/src/reflection-mapped-metadata.d.ts.map +1 -0
  115. package/dist/src/reflection-mapped-metadata.js +40 -0
  116. package/dist/src/reflection-mapped-metadata.js.map +7 -0
  117. package/dist/src/reflection-metadata.d.ts +11 -0
  118. package/dist/src/reflection-metadata.d.ts.map +1 -0
  119. package/dist/src/reflection-metadata.js +24 -0
  120. package/dist/src/reflection-metadata.js.map +7 -0
  121. package/dist/src/reflection-ranking.d.ts +13 -0
  122. package/dist/src/reflection-ranking.d.ts.map +1 -0
  123. package/{src/reflection-ranking.ts → dist/src/reflection-ranking.js} +12 -21
  124. package/dist/src/reflection-ranking.js.map +7 -0
  125. package/dist/src/reflection-retry.d.ts +30 -0
  126. package/dist/src/reflection-retry.d.ts.map +1 -0
  127. package/{src/reflection-retry.ts → dist/src/reflection-retry.js} +24 -64
  128. package/dist/src/reflection-retry.js.map +7 -0
  129. package/dist/src/reflection-slices.d.ts +42 -0
  130. package/dist/src/reflection-slices.d.ts.map +1 -0
  131. package/{src/reflection-slices.ts → dist/src/reflection-slices.js} +60 -136
  132. package/dist/src/reflection-slices.js.map +7 -0
  133. package/dist/src/reflection-store.d.ts +85 -0
  134. package/dist/src/reflection-store.d.ts.map +1 -0
  135. package/dist/src/reflection-store.js +407 -0
  136. package/dist/src/reflection-store.js.map +7 -0
  137. package/dist/src/resonance-state.d.ts +19 -0
  138. package/dist/src/resonance-state.d.ts.map +1 -0
  139. package/{src/resonance-state.ts → dist/src/resonance-state.js} +13 -42
  140. package/dist/src/resonance-state.js.map +7 -0
  141. package/dist/src/retriever.d.ts +228 -0
  142. package/dist/src/retriever.d.ts.map +1 -0
  143. package/dist/src/retriever.js +1006 -0
  144. package/dist/src/retriever.js.map +7 -0
  145. package/dist/src/scopes.d.ts +58 -0
  146. package/dist/src/scopes.d.ts.map +1 -0
  147. package/dist/src/scopes.js +252 -0
  148. package/dist/src/scopes.js.map +7 -0
  149. package/dist/src/self-improvement-files.d.ts +20 -0
  150. package/dist/src/self-improvement-files.d.ts.map +1 -0
  151. package/{src/self-improvement-files.ts → dist/src/self-improvement-files.js} +24 -49
  152. package/dist/src/self-improvement-files.js.map +7 -0
  153. package/dist/src/semantic-gate.d.ts +24 -0
  154. package/dist/src/semantic-gate.d.ts.map +1 -0
  155. package/dist/src/semantic-gate.js +86 -0
  156. package/dist/src/semantic-gate.js.map +7 -0
  157. package/dist/src/session-recovery.d.ts +9 -0
  158. package/dist/src/session-recovery.d.ts.map +1 -0
  159. package/{src/session-recovery.ts → dist/src/session-recovery.js} +40 -57
  160. package/dist/src/session-recovery.js.map +7 -0
  161. package/dist/src/smart-extractor.d.ts +107 -0
  162. package/dist/src/smart-extractor.d.ts.map +1 -0
  163. package/{src/smart-extractor.ts → dist/src/smart-extractor.js} +130 -383
  164. package/dist/src/smart-extractor.js.map +7 -0
  165. package/dist/src/smart-metadata.d.ts +103 -0
  166. package/dist/src/smart-metadata.d.ts.map +1 -0
  167. package/dist/src/smart-metadata.js +361 -0
  168. package/dist/src/smart-metadata.js.map +7 -0
  169. package/dist/src/storage-adapter.d.ts +102 -0
  170. package/dist/src/storage-adapter.d.ts.map +1 -0
  171. package/dist/src/storage-adapter.js +22 -0
  172. package/dist/src/storage-adapter.js.map +7 -0
  173. package/dist/src/store.d.ts +108 -0
  174. package/dist/src/store.d.ts.map +1 -0
  175. package/dist/src/store.js +939 -0
  176. package/dist/src/store.js.map +7 -0
  177. package/dist/src/tier-manager.d.ts +57 -0
  178. package/dist/src/tier-manager.d.ts.map +1 -0
  179. package/dist/src/tier-manager.js +80 -0
  180. package/dist/src/tier-manager.js.map +7 -0
  181. package/dist/src/tools.d.ts +43 -0
  182. package/dist/src/tools.d.ts.map +1 -0
  183. package/dist/src/tools.js +1075 -0
  184. package/dist/src/tools.js.map +7 -0
  185. package/dist/src/wal-recovery.d.ts +30 -0
  186. package/dist/src/wal-recovery.d.ts.map +1 -0
  187. package/{src/wal-recovery.ts → dist/src/wal-recovery.js} +26 -79
  188. package/dist/src/wal-recovery.js.map +7 -0
  189. package/package.json +21 -2
  190. package/openclaw.plugin.json +0 -815
  191. package/src/access-tracker.ts +0 -341
  192. package/src/adapters/README.md +0 -78
  193. package/src/adapters/qdrant.ts +0 -191
  194. package/src/adaptive-retrieval.ts +0 -90
  195. package/src/audit-log.ts +0 -238
  196. package/src/chunker.ts +0 -254
  197. package/src/config.ts +0 -271
  198. package/src/decay-engine.ts +0 -238
  199. package/src/extraction-prompts.ts +0 -339
  200. package/src/memory-categories.ts +0 -71
  201. package/src/memory-upgrader.ts +0 -388
  202. package/src/mnemo.ts +0 -142
  203. package/src/noise-filter.ts +0 -97
  204. package/src/noise-prototypes.ts +0 -164
  205. package/src/observability.ts +0 -81
  206. package/src/query-tracker.ts +0 -57
  207. package/src/reflection-event-store.ts +0 -98
  208. package/src/reflection-item-store.ts +0 -112
  209. package/src/reflection-mapped-metadata.ts +0 -84
  210. package/src/reflection-metadata.ts +0 -23
  211. package/src/reflection-store.ts +0 -602
  212. package/src/retriever.ts +0 -1510
  213. package/src/scopes.ts +0 -375
  214. package/src/semantic-gate.ts +0 -121
  215. package/src/smart-metadata.ts +0 -561
  216. package/src/storage-adapter.ts +0 -153
  217. package/src/store.ts +0 -1330
  218. package/src/tier-manager.ts +0 -189
  219. package/src/tools.ts +0 -1292
  220. package/test/core.test.mjs +0 -301
@@ -1,561 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- import type { MemoryCategory, MemoryTier } from "./memory-categories.js";
3
- import type { DecayableMemory } from "./decay-engine.js";
4
-
5
- type LegacyStoreCategory =
6
- | "preference"
7
- | "fact"
8
- | "decision"
9
- | "entity"
10
- | "other"
11
- | "reflection";
12
-
13
- type EntryLike = {
14
- text?: string;
15
- category?: LegacyStoreCategory;
16
- importance?: number;
17
- timestamp?: number;
18
- metadata?: string;
19
- };
20
-
21
- export interface SmartMemoryMetadata {
22
- l0_abstract: string;
23
- l1_overview: string;
24
- l2_content: string;
25
- memory_category: MemoryCategory;
26
- tier: MemoryTier;
27
- access_count: number;
28
- confidence: number;
29
- last_accessed_at: number;
30
- source_session?: string;
31
- /** Emotional salience score (0-1). Higher = more emotionally significant = decays slower.
32
- * Computed at store time via heuristic rules (zero LLM cost). */
33
- emotional_salience: number;
34
- [key: string]: unknown;
35
- }
36
-
37
- export interface LifecycleMemory {
38
- id: string;
39
- importance: number;
40
- confidence: number;
41
- tier: MemoryTier;
42
- accessCount: number;
43
- createdAt: number;
44
- lastAccessedAt: number;
45
- emotionalSalience: number;
46
- }
47
-
48
- function clamp01(value: unknown, fallback: number): number {
49
- const n = typeof value === "number" ? value : Number(value);
50
- if (!Number.isFinite(n)) return fallback;
51
- return Math.min(1, Math.max(0, n));
52
- }
53
-
54
- function clampCount(value: unknown, fallback = 0): number {
55
- const n = typeof value === "number" ? value : Number(value);
56
- if (!Number.isFinite(n) || n < 0) return fallback;
57
- return Math.floor(n);
58
- }
59
-
60
- // ============================================================================
61
- // Emotional Salience — heuristic scoring (zero LLM cost)
62
- // ============================================================================
63
-
64
- /** High-salience signal patterns (decisions, strong emotions, firsts, money, people) */
65
- const SALIENCE_BOOSTERS: Array<{ pattern: RegExp; boost: number }> = [
66
- // Decisions and commitments
67
- { pattern: /\b(决定|决策|confirmed|decided|commit|approved|批了|拍板|定了)\b/i, boost: 0.3 },
68
- // Strong emotions
69
- { pattern: /\b(震惊|惊喜|愤怒|失望|兴奋|amazing|shocked|frustrated|excited|worried|担心)\b/i, boost: 0.25 },
70
- // First-time events
71
- { pattern: /\b(第一次|首次|first time|first ever|从未|never before)\b/i, boost: 0.25 },
72
- // Financial significance
73
- { pattern: /\b(\d+万|\d+亿|\$[\d,.]+[MBK]|估值|valuation|投资|持仓)\b/i, boost: 0.2 },
74
- // People and relationships (user-configurable entity names)
75
- { pattern: /\b(colleague|partner|mentor|friend|manager|teammate)\b/i, boost: 0.15 },
76
- // Lessons learned / mistakes
77
- { pattern: /\b(教训|踩坑|pitfall|lesson|mistake|bug|故障|挂了|崩了)\b/i, boost: 0.2 },
78
- // Preferences and identity
79
- { pattern: /\b(喜欢|讨厌|偏好|prefer|hate|love|always|never)\b/i, boost: 0.15 },
80
- // Exclamation / emphasis (emotional weight)
81
- { pattern: /[!!]{2,}|‼️|⚠️|🔴|💀/, boost: 0.1 },
82
- ];
83
-
84
- /** Low-salience patterns (routine, technical noise) */
85
- const SALIENCE_DAMPENERS: Array<{ pattern: RegExp; dampen: number }> = [
86
- { pattern: /\b(heartbeat|HEARTBEAT_OK)\b/i, dampen: 0.2 },
87
- { pattern: /\b(cron|restart|gateway|status)\b/i, dampen: 0.1 },
88
- { pattern: /\b(debug|stack trace|npm|node_modules|\.tsx?|\.jsx?)\b/i, dampen: 0.1 },
89
- { pattern: /^\[?(Updated|Added|Removed|Fixed|Set)\]?\s.{0,30}$/i, dampen: 0.15 }, // short auto-generated entries only
90
- ];
91
-
92
- /**
93
- * Compute emotional salience from text + category.
94
- * Returns 0-1. Higher = more emotionally charged / personally significant.
95
- * Pure heuristic, no LLM call.
96
- */
97
- export function computeEmotionalSalience(
98
- text: string,
99
- category?: string,
100
- importance?: number,
101
- ): number {
102
- let score = 0.35; // baseline — neutral memory
103
-
104
- // Category boost
105
- if (category === "decision") score += 0.15;
106
- if (category === "preference") score += 0.1;
107
- if (category === "reflection") score += 0.1;
108
-
109
- // Importance as a weak signal
110
- if (typeof importance === "number" && importance > 0.8) score += 0.1;
111
- if (typeof importance === "number" && importance > 0.9) score += 0.05;
112
-
113
- // Pattern matching
114
- for (const { pattern, boost } of SALIENCE_BOOSTERS) {
115
- if (pattern.test(text)) score += boost;
116
- }
117
- for (const { pattern, dampen } of SALIENCE_DAMPENERS) {
118
- if (pattern.test(text)) score -= dampen;
119
- }
120
-
121
- return Math.min(1, Math.max(0, score));
122
- }
123
-
124
- // ============================================================================
125
- // Emotion Calibration — rule-based post-processing for LLM valence
126
- // ============================================================================
127
-
128
- /** Strong emotion signal words (positive or negative) */
129
- const STRONG_EMOTION_PATTERNS = [
130
- /太好了|太棒了|amazing|incredible|wonderful|fantastic|excellent/i,
131
- /terrible|horrible|awful|disgusting|devastating/i,
132
- /fuck|shit|damn|靠|卧槽|我去|妈的/i,
133
- /哈哈哈|lol|lmao|rofl|😂|🤣/i,
134
- /[!!]{3,}/,
135
- /heartbroken|ecstatic|furious|thrilled|terrified/i,
136
- /崩溃|暴怒|狂喜|绝望|震惊|兴奋死了/i,
137
- ];
138
-
139
- /** Factual / data-heavy text indicators */
140
- const FACTUAL_PATTERNS = [
141
- /估值|revenue|valuation|profit|loss|margin/i,
142
- /[$¥€£]/,
143
- /%/,
144
- ];
145
-
146
- /**
147
- * Calibrate LLM-returned emotion valence with rule-based post-processing.
148
- *
149
- * Fixes two common LLM failure modes:
150
- * 1. Strong emotional text scored as neutral (0.4-0.6) → push toward extremes
151
- * 2. Pure factual/data text scored as emotional → compress toward neutral
152
- *
153
- * @param text - The memory text
154
- * @param rawValence - LLM-returned valence (0-1, 0.5 = neutral)
155
- * @returns Calibrated valence (0-1)
156
- */
157
- export function calibrateEmotion(text: string, rawValence: number): number {
158
- if (!Number.isFinite(rawValence)) return 0.5;
159
-
160
- // Check for strong emotion signals
161
- const hasStrongEmotion = STRONG_EMOTION_PATTERNS.some(p => p.test(text));
162
-
163
- // Check if text is factual/data-heavy
164
- const digitChars = (text.match(/\d/g) || []).length;
165
- const digitRatio = text.length > 0 ? digitChars / text.length : 0;
166
- const hasFactualSignals = digitRatio > 0.3 || FACTUAL_PATTERNS.some(p => p.test(text));
167
-
168
- let calibrated = rawValence;
169
-
170
- // Fix 1: Strong emotion + neutral valence → push to 0.3 or 0.7
171
- if (hasStrongEmotion && rawValence >= 0.4 && rawValence <= 0.6) {
172
- // Determine direction: below 0.5 → negative, above → positive
173
- calibrated = rawValence <= 0.5 ? 0.3 : 0.7;
174
- }
175
-
176
- // Fix 2: Factual text → compress to 0.45-0.55
177
- if (hasFactualSignals && !hasStrongEmotion) {
178
- if (calibrated < 0.45) calibrated = 0.45;
179
- if (calibrated > 0.55) calibrated = 0.55;
180
- }
181
-
182
- return Math.min(1, Math.max(0, calibrated));
183
- }
184
-
185
- function normalizeTier(value: unknown): MemoryTier {
186
- switch (value) {
187
- case "core":
188
- case "working":
189
- case "peripheral":
190
- return value;
191
- default:
192
- return "working";
193
- }
194
- }
195
-
196
- export function reverseMapLegacyCategory(
197
- oldCategory: LegacyStoreCategory | undefined,
198
- text = "",
199
- ): MemoryCategory {
200
- switch (oldCategory) {
201
- case "preference":
202
- return "preferences";
203
- case "entity":
204
- return "entities";
205
- case "decision":
206
- return "events";
207
- case "other":
208
- return "patterns";
209
- case "fact":
210
- if (
211
- /\b(my |i am |i'm |name is |叫我|我的|我是)\b/i.test(text) &&
212
- text.length < 200
213
- ) {
214
- return "profile";
215
- }
216
- return "cases";
217
- default:
218
- return "patterns";
219
- }
220
- }
221
-
222
- function defaultOverview(text: string): string {
223
- return `- ${text}`;
224
- }
225
-
226
- function normalizeText(value: unknown, fallback: string): string {
227
- return typeof value === "string" && value.trim() ? value.trim() : fallback;
228
- }
229
-
230
- export function parseSmartMetadata(
231
- rawMetadata: string | undefined,
232
- entry: EntryLike = {},
233
- ): SmartMemoryMetadata {
234
- let parsed: Record<string, unknown> = {};
235
- if (rawMetadata) {
236
- try {
237
- const obj = JSON.parse(rawMetadata);
238
- if (obj && typeof obj === "object") {
239
- parsed = obj as Record<string, unknown>;
240
- }
241
- } catch {
242
- parsed = {};
243
- }
244
- }
245
-
246
- const text = entry.text ?? "";
247
- const timestamp =
248
- typeof entry.timestamp === "number" && Number.isFinite(entry.timestamp)
249
- ? entry.timestamp
250
- : Date.now();
251
-
252
- const memoryCategory = reverseMapLegacyCategory(entry.category, text);
253
- const l0 = normalizeText(parsed.l0_abstract, text);
254
- const l2 = normalizeText(parsed.l2_content, text);
255
- const normalized: SmartMemoryMetadata = {
256
- ...parsed,
257
- l0_abstract: l0,
258
- l1_overview: normalizeText(parsed.l1_overview, defaultOverview(l0)),
259
- l2_content: l2,
260
- memory_category:
261
- typeof parsed.memory_category === "string"
262
- ? (parsed.memory_category as MemoryCategory)
263
- : memoryCategory,
264
- tier: normalizeTier(parsed.tier),
265
- access_count: clampCount(parsed.access_count, 0),
266
- confidence: clamp01(parsed.confidence, 0.7),
267
- last_accessed_at: clampCount(parsed.last_accessed_at, timestamp),
268
- source_session:
269
- typeof parsed.source_session === "string" ? parsed.source_session : undefined,
270
- emotional_salience: clamp01(
271
- parsed.emotional_salience,
272
- computeEmotionalSalience(text, entry.category, entry.importance),
273
- ),
274
- };
275
-
276
- return normalized;
277
- }
278
-
279
- export function buildSmartMetadata(
280
- entry: EntryLike,
281
- patch: Partial<SmartMemoryMetadata> = {},
282
- ): SmartMemoryMetadata {
283
- const base = parseSmartMetadata(entry.metadata, entry);
284
- const text = entry.text ?? "";
285
-
286
- // Calibrate emotional salience: fix LLM mis-scoring of strong emotions / factual text
287
- const rawSalience = clamp01(
288
- patch.emotional_salience ?? base.emotional_salience,
289
- base.emotional_salience,
290
- );
291
- const calibratedSalience = calibrateEmotion(text, rawSalience);
292
-
293
- return {
294
- ...base,
295
- ...patch,
296
- l0_abstract: normalizeText(patch.l0_abstract, base.l0_abstract),
297
- l1_overview: normalizeText(patch.l1_overview, base.l1_overview),
298
- l2_content: normalizeText(patch.l2_content, base.l2_content),
299
- memory_category:
300
- typeof patch.memory_category === "string"
301
- ? patch.memory_category
302
- : base.memory_category,
303
- tier: normalizeTier(patch.tier ?? base.tier),
304
- access_count: clampCount(patch.access_count, base.access_count),
305
- confidence: clamp01(patch.confidence, base.confidence),
306
- last_accessed_at: clampCount(
307
- patch.last_accessed_at,
308
- base.last_accessed_at || entry.timestamp || Date.now(),
309
- ),
310
- source_session:
311
- typeof patch.source_session === "string"
312
- ? patch.source_session
313
- : base.source_session,
314
- emotional_salience: calibratedSalience,
315
- };
316
- }
317
-
318
- // Metadata array size caps — prevent unbounded JSON growth
319
- const MAX_SOURCES = 20;
320
- const MAX_HISTORY = 50;
321
- const MAX_RELATIONS = 16;
322
-
323
- export function stringifySmartMetadata(
324
- metadata: SmartMemoryMetadata | Record<string, unknown>,
325
- ): string {
326
- const capped = { ...metadata } as Record<string, unknown>;
327
-
328
- // Cap array fields to prevent metadata bloat
329
- if (Array.isArray(capped.sources) && capped.sources.length > MAX_SOURCES) {
330
- capped.sources = capped.sources.slice(-MAX_SOURCES); // keep most recent
331
- }
332
- if (Array.isArray(capped.history) && capped.history.length > MAX_HISTORY) {
333
- capped.history = capped.history.slice(-MAX_HISTORY);
334
- }
335
- if (Array.isArray(capped.relations) && capped.relations.length > MAX_RELATIONS) {
336
- capped.relations = capped.relations.slice(0, MAX_RELATIONS);
337
- }
338
-
339
- return JSON.stringify(capped);
340
- }
341
-
342
- export function toLifecycleMemory(
343
- id: string,
344
- entry: EntryLike,
345
- ): LifecycleMemory {
346
- const metadata = parseSmartMetadata(entry.metadata, entry);
347
- const createdAt =
348
- typeof entry.timestamp === "number" && Number.isFinite(entry.timestamp)
349
- ? entry.timestamp
350
- : Date.now();
351
-
352
- return {
353
- id,
354
- importance:
355
- typeof entry.importance === "number" && Number.isFinite(entry.importance)
356
- ? entry.importance
357
- : 0.7,
358
- confidence: metadata.confidence,
359
- tier: metadata.tier,
360
- accessCount: metadata.access_count,
361
- createdAt,
362
- lastAccessedAt: metadata.last_accessed_at || createdAt,
363
- emotionalSalience: metadata.emotional_salience,
364
- };
365
- }
366
-
367
- /**
368
- * Parse a memory entry into both a DecayableMemory (for the decay engine)
369
- * and the raw SmartMemoryMetadata (for in-place mutation before write-back).
370
- */
371
- export function getDecayableFromEntry(
372
- entry: EntryLike & { id?: string },
373
- ): { memory: DecayableMemory; meta: SmartMemoryMetadata } {
374
- const meta = parseSmartMetadata(entry.metadata, entry);
375
- const createdAt =
376
- typeof entry.timestamp === "number" && Number.isFinite(entry.timestamp)
377
- ? entry.timestamp
378
- : Date.now();
379
-
380
- const memory: DecayableMemory = {
381
- id: (entry as { id?: string }).id ?? "",
382
- importance:
383
- typeof entry.importance === "number" && Number.isFinite(entry.importance)
384
- ? entry.importance
385
- : 0.7,
386
- confidence: meta.confidence,
387
- tier: meta.tier,
388
- accessCount: meta.access_count,
389
- createdAt,
390
- lastAccessedAt: meta.last_accessed_at || createdAt,
391
- emotionalSalience: meta.emotional_salience,
392
- };
393
-
394
- return { memory, meta };
395
- }
396
-
397
- // ============================================================================
398
- // Contextual Support — optional extension to SmartMemoryMetadata
399
- // ============================================================================
400
-
401
- /** Predefined context vocabulary for support slices */
402
- export const SUPPORT_CONTEXT_VOCABULARY = [
403
- "general", "morning", "afternoon", "evening", "night",
404
- "weekday", "weekend", "work", "leisure",
405
- "summer", "winter", "travel",
406
- ] as const;
407
-
408
- export type SupportContext = (typeof SUPPORT_CONTEXT_VOCABULARY)[number] | string;
409
-
410
- /** Max number of context slices per memory to prevent metadata bloat */
411
- export const MAX_SUPPORT_SLICES = 8;
412
-
413
- /** A single context-specific support slice */
414
- export interface ContextualSupport {
415
- context: SupportContext;
416
- confirmations: number;
417
- contradictions: number;
418
- strength: number; // confirmations / (confirmations + contradictions)
419
- last_observed_at: number;
420
- }
421
-
422
- /** V2 support info with per-context slices */
423
- export interface SupportInfoV2 {
424
- global_strength: number; // weighted average across all slices
425
- total_observations: number; // sum of all confirmations + contradictions
426
- slices: ContextualSupport[];
427
- }
428
-
429
- /**
430
- * Normalize a raw context label to a canonical context.
431
- * Maps common variants and falls back to "general".
432
- */
433
- export function normalizeContext(raw: string | undefined): SupportContext {
434
- if (!raw || !raw.trim()) return "general";
435
- const lower = raw.trim().toLowerCase();
436
-
437
- // Direct vocabulary match
438
- if ((SUPPORT_CONTEXT_VOCABULARY as readonly string[]).includes(lower)) {
439
- return lower as SupportContext;
440
- }
441
-
442
- // Common Chinese/English mappings
443
- const aliases: Record<string, SupportContext> = {
444
- "早上": "morning", "上午": "morning", "早晨": "morning",
445
- "下午": "afternoon", "傍晚": "evening", "晚上": "evening",
446
- "深夜": "night", "夜晚": "night", "凌晨": "night",
447
- "工作日": "weekday", "平时": "weekday",
448
- "周末": "weekend", "假日": "weekend", "休息日": "weekend",
449
- "工作": "work", "上班": "work", "办公": "work",
450
- "休闲": "leisure", "放松": "leisure", "休息": "leisure",
451
- "夏天": "summer", "夏季": "summer",
452
- "冬天": "winter", "冬季": "winter",
453
- "旅行": "travel", "出差": "travel", "旅游": "travel",
454
- };
455
-
456
- return aliases[lower] || lower; // keep as custom context if not mapped
457
- }
458
-
459
- /**
460
- * Parse support_info from metadata JSON. Handles V1 (flat) → V2 (sliced) migration.
461
- */
462
- export function parseSupportInfo(raw: unknown): SupportInfoV2 {
463
- const defaultV2: SupportInfoV2 = {
464
- global_strength: 0.5,
465
- total_observations: 0,
466
- slices: [],
467
- };
468
-
469
- if (!raw || typeof raw !== "object") return defaultV2;
470
- const obj = raw as Record<string, unknown>;
471
-
472
- // V2 format: has slices array
473
- if (Array.isArray(obj.slices)) {
474
- return {
475
- global_strength: typeof obj.global_strength === "number" ? obj.global_strength : 0.5,
476
- total_observations: typeof obj.total_observations === "number" ? obj.total_observations : 0,
477
- slices: (obj.slices as Record<string, unknown>[]).filter(
478
- s => s && typeof s.context === "string",
479
- ).map(s => ({
480
- context: String(s.context),
481
- confirmations: typeof s.confirmations === "number" && s.confirmations >= 0 ? s.confirmations : 0,
482
- contradictions: typeof s.contradictions === "number" && s.contradictions >= 0 ? s.contradictions : 0,
483
- strength: typeof s.strength === "number" && s.strength >= 0 && s.strength <= 1 ? s.strength : 0.5,
484
- last_observed_at: typeof s.last_observed_at === "number" ? s.last_observed_at : Date.now(),
485
- })),
486
- };
487
- }
488
-
489
- // V1 format: flat { confirmations, contradictions, strength }
490
- const conf = typeof obj.confirmations === "number" ? obj.confirmations : 0;
491
- const contra = typeof obj.contradictions === "number" ? obj.contradictions : 0;
492
- const total = conf + contra;
493
- if (total === 0) return defaultV2;
494
-
495
- return {
496
- global_strength: total > 0 ? conf / total : 0.5,
497
- total_observations: total,
498
- slices: [{
499
- context: "general",
500
- confirmations: conf,
501
- contradictions: contra,
502
- strength: total > 0 ? conf / total : 0.5,
503
- last_observed_at: Date.now(),
504
- }],
505
- };
506
- }
507
-
508
- /**
509
- * Update support stats for a specific context.
510
- * Returns a new SupportInfoV2 with the updated slice.
511
- */
512
- export function updateSupportStats(
513
- existing: SupportInfoV2,
514
- contextLabel: string | undefined,
515
- event: "support" | "contradict",
516
- ): SupportInfoV2 {
517
- const ctx = normalizeContext(contextLabel);
518
- const base = { ...existing, slices: [...existing.slices.map(s => ({ ...s }))] };
519
-
520
- // Find or create the context slice
521
- let slice = base.slices.find(s => s.context === ctx);
522
- if (!slice) {
523
- slice = { context: ctx, confirmations: 0, contradictions: 0, strength: 0.5, last_observed_at: Date.now() };
524
- base.slices.push(slice);
525
- }
526
-
527
- // Update slice
528
- if (event === "support") slice.confirmations++;
529
- else slice.contradictions++;
530
- const sliceTotal = slice.confirmations + slice.contradictions;
531
- slice.strength = sliceTotal > 0 ? slice.confirmations / sliceTotal : 0.5;
532
- slice.last_observed_at = Date.now();
533
-
534
- // Cap slices (keep most recently observed, but preserve dropped evidence).
535
- // NOTE: Evidence from slices dropped in *previous* updates is already baked
536
- // into total_observations/global_strength, so those values may drift slightly
537
- // over many truncation cycles. This is an accepted trade-off for bounded JSON size.
538
- let slices = base.slices;
539
- let droppedConf = 0, droppedContra = 0;
540
- if (slices.length > MAX_SUPPORT_SLICES) {
541
- slices = slices
542
- .sort((a, b) => b.last_observed_at - a.last_observed_at);
543
- const dropped = slices.slice(MAX_SUPPORT_SLICES);
544
- for (const d of dropped) {
545
- droppedConf += d.confirmations;
546
- droppedContra += d.contradictions;
547
- }
548
- slices = slices.slice(0, MAX_SUPPORT_SLICES);
549
- }
550
-
551
- // Recompute global strength including evidence from dropped slices
552
- let totalConf = droppedConf, totalContra = droppedContra;
553
- for (const s of slices) {
554
- totalConf += s.confirmations;
555
- totalContra += s.contradictions;
556
- }
557
- const totalObs = totalConf + totalContra;
558
- const global_strength = totalObs > 0 ? totalConf / totalObs : 0.5;
559
-
560
- return { global_strength, total_observations: totalObs, slices };
561
- }
@@ -1,153 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- /**
3
- * StorageAdapter — Abstract interface for vector storage backends.
4
- *
5
- * Mnemo ships with LanceDB as the default adapter.
6
- * Implement this interface to add support for other backends
7
- * (Qdrant, Chroma, Milvus, Pinecone, PGVector, etc.)
8
- */
9
-
10
- // ── Types ──
11
-
12
- export interface MemoryRecord {
13
- id: string;
14
- text: string;
15
- vector: number[];
16
- timestamp: number;
17
- scope: string;
18
- importance: number;
19
- category: string;
20
- metadata: string;
21
- [key: string]: unknown;
22
- }
23
-
24
- export interface SearchResult {
25
- record: MemoryRecord;
26
- score: number;
27
- }
28
-
29
- export interface QueryOptions {
30
- where?: string;
31
- select?: string[];
32
- limit?: number;
33
- offset?: number;
34
- }
35
-
36
- // ── Interface ──
37
-
38
- export interface StorageAdapter {
39
- /** Adapter name for logging and diagnostics */
40
- readonly name: string;
41
-
42
- /**
43
- * Connect to the storage backend.
44
- * @param dbPath - path or connection string
45
- */
46
- connect(dbPath: string): Promise<void>;
47
-
48
- /**
49
- * Ensure the memories table exists with proper schema.
50
- * Creates the table if it doesn't exist, opens it if it does.
51
- * @param vectorDimensions - embedding vector dimensions (e.g. 1024)
52
- */
53
- ensureTable(vectorDimensions: number): Promise<void>;
54
-
55
- /**
56
- * Insert one or more records.
57
- */
58
- add(records: MemoryRecord[]): Promise<void>;
59
-
60
- /**
61
- * Update a record by ID. Typically delete + re-insert.
62
- */
63
- update(id: string, record: MemoryRecord): Promise<void>;
64
-
65
- /**
66
- * Delete records matching a filter expression.
67
- * @param filter - SQL-like filter string, e.g. "id = 'abc'"
68
- */
69
- delete(filter: string): Promise<void>;
70
-
71
- /**
72
- * Vector similarity search.
73
- * @returns results sorted by descending similarity score (0-1)
74
- */
75
- vectorSearch(
76
- vector: number[],
77
- limit: number,
78
- minScore?: number,
79
- scopeFilter?: string[],
80
- ): Promise<SearchResult[]>;
81
-
82
- /**
83
- * Full-text (BM25) search.
84
- * @returns results sorted by descending relevance score
85
- */
86
- fullTextSearch(
87
- query: string,
88
- limit: number,
89
- scopeFilter?: string[],
90
- ): Promise<SearchResult[]>;
91
-
92
- /**
93
- * General-purpose query with optional filter, select, limit.
94
- */
95
- query(options: QueryOptions): Promise<MemoryRecord[]>;
96
-
97
- /**
98
- * Count records matching an optional filter.
99
- */
100
- count(filter?: string): Promise<number>;
101
-
102
- /**
103
- * Ensure full-text search index exists on the text field.
104
- */
105
- ensureFullTextIndex(): Promise<void>;
106
-
107
- /**
108
- * Check if full-text search is supported and initialized.
109
- */
110
- hasFullTextSearch(): boolean;
111
-
112
- /**
113
- * Close the connection / cleanup resources.
114
- */
115
- close(): Promise<void>;
116
- }
117
-
118
- // ── Factory ──
119
-
120
- export type AdapterFactory = (config?: Record<string, unknown>) => StorageAdapter;
121
-
122
- const _registry = new Map<string, AdapterFactory>();
123
-
124
- /**
125
- * Register a storage adapter backend.
126
- * @example registerAdapter("qdrant", (config) => new QdrantAdapter(config))
127
- */
128
- export function registerAdapter(name: string, factory: AdapterFactory): void {
129
- _registry.set(name, factory);
130
- }
131
-
132
- /**
133
- * Create a storage adapter by name.
134
- * Falls back to LanceDB if name is not registered.
135
- */
136
- export function createAdapter(name: string, config?: Record<string, unknown>): StorageAdapter {
137
- const factory = _registry.get(name);
138
- if (!factory) {
139
- throw new Error(
140
- `Storage adapter "${name}" not found. ` +
141
- `Available: ${[..._registry.keys()].join(", ") || "(none)"}. ` +
142
- `Did you forget to call registerAdapter()?`
143
- );
144
- }
145
- return factory(config);
146
- }
147
-
148
- /**
149
- * List all registered adapter names.
150
- */
151
- export function listAdapters(): string[] {
152
- return [..._registry.keys()];
153
- }