@jungjaehoon/mama-core 1.0.4 → 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.
@@ -1,222 +0,0 @@
1
- /**
2
- * MAMA (Memory-Augmented MCP Architecture) - Embedding Cache
3
- *
4
- * LRU cache for embedding vectors to avoid re-computation
5
- *
6
- * Task 2: Implement Embedding Cache
7
- * AC #3: Cache embeddings to avoid re-computation
8
- * Target: > 80% cache hit ratio
9
- *
10
- * @module embedding-cache
11
- * @version 1.0
12
- * @date 2025-11-14
13
- */
14
-
15
- const crypto = require('crypto');
16
-
17
- // Cache configuration
18
- const MAX_CACHE_SIZE = 1000; // Max 1000 entries (story req 2.2)
19
- const CLEANUP_THRESHOLD = 1100; // Trigger cleanup at 110%
20
-
21
- /**
22
- * LRU Cache for embeddings
23
- *
24
- * Key: SHA-256 hash of decision text
25
- * Value: {embedding: Float32Array, timestamp: number, hits: number}
26
- *
27
- * Eviction: Least Recently Used when size > MAX_CACHE_SIZE
28
- */
29
- class EmbeddingCache {
30
- constructor() {
31
- this.cache = new Map();
32
- this.stats = {
33
- hits: 0,
34
- misses: 0,
35
- evictions: 0,
36
- totalSize: 0,
37
- };
38
- }
39
-
40
- /**
41
- * Generate cache key from text
42
- *
43
- * Task 2.3: Key = decision text hash (SHA-256)
44
- *
45
- * @param {string} text - Input text
46
- * @returns {string} SHA-256 hex hash
47
- */
48
- generateKey(text) {
49
- if (!text || typeof text !== 'string') {
50
- throw new Error('Text must be a non-empty string');
51
- }
52
-
53
- return crypto.createHash('sha256').update(text).digest('hex');
54
- }
55
-
56
- /**
57
- * Get embedding from cache
58
- *
59
- * Task 2.2: Cache hit updates LRU position
60
- *
61
- * @param {string} text - Input text
62
- * @returns {Float32Array|null} Cached embedding or null
63
- */
64
- get(text) {
65
- const key = this.generateKey(text);
66
- const entry = this.cache.get(key);
67
-
68
- if (entry) {
69
- // Cache hit - update LRU position
70
- this.stats.hits++;
71
- entry.hits++;
72
- entry.lastAccessed = Date.now();
73
-
74
- // Move to end (most recently used)
75
- this.cache.delete(key);
76
- this.cache.set(key, entry);
77
-
78
- return entry.embedding;
79
- }
80
-
81
- // Cache miss
82
- this.stats.misses++;
83
- return null;
84
- }
85
-
86
- /**
87
- * Store embedding in cache
88
- *
89
- * Task 2.2: Add to cache with LRU tracking
90
- * Task 2.5: Implement cache eviction (LRU)
91
- *
92
- * @param {string} text - Input text
93
- * @param {Float32Array} embedding - Embedding vector
94
- */
95
- set(text, embedding) {
96
- const key = this.generateKey(text);
97
-
98
- // Check if already exists (update case)
99
- if (this.cache.has(key)) {
100
- const entry = this.cache.get(key);
101
- entry.embedding = embedding;
102
- entry.lastAccessed = Date.now();
103
-
104
- // Move to end (most recently used)
105
- this.cache.delete(key);
106
- this.cache.set(key, entry);
107
- return;
108
- }
109
-
110
- // New entry
111
- const entry = {
112
- embedding,
113
- timestamp: Date.now(),
114
- lastAccessed: Date.now(),
115
- hits: 0,
116
- };
117
-
118
- this.cache.set(key, entry);
119
- this.stats.totalSize++;
120
-
121
- // Trigger cleanup if threshold exceeded
122
- if (this.cache.size > CLEANUP_THRESHOLD) {
123
- this.evictLRU();
124
- }
125
- }
126
-
127
- /**
128
- * Evict least recently used entries
129
- *
130
- * Task 2.5: Implement cache eviction (LRU)
131
- * Strategy: Remove entries until size <= MAX_CACHE_SIZE
132
- *
133
- * Eviction order:
134
- * 1. Oldest lastAccessed (LRU)
135
- * 2. If tied, lowest hits
136
- */
137
- evictLRU() {
138
- const targetEvictions = this.cache.size - MAX_CACHE_SIZE;
139
-
140
- if (targetEvictions <= 0) {
141
- return;
142
- }
143
-
144
- // Convert to array for sorting
145
- const entries = Array.from(this.cache.entries());
146
-
147
- // Sort by LRU (oldest first), then by hits (lowest first)
148
- entries.sort((a, b) => {
149
- const [, entryA] = a;
150
- const [, entryB] = b;
151
-
152
- // Primary: lastAccessed (ascending - oldest first)
153
- if (entryA.lastAccessed !== entryB.lastAccessed) {
154
- return entryA.lastAccessed - entryB.lastAccessed;
155
- }
156
-
157
- // Secondary: hits (ascending - lowest first)
158
- return entryA.hits - entryB.hits;
159
- });
160
-
161
- // Evict oldest entries
162
- for (let i = 0; i < targetEvictions; i++) {
163
- const [key] = entries[i];
164
- this.cache.delete(key);
165
- this.stats.evictions++;
166
- this.stats.totalSize--;
167
- }
168
- }
169
-
170
- /**
171
- * Get cache hit ratio
172
- *
173
- * Task 2.4: Cache hit ratio target: > 80%
174
- *
175
- * @returns {number} Hit ratio (0.0 - 1.0)
176
- */
177
- getHitRatio() {
178
- const total = this.stats.hits + this.stats.misses;
179
-
180
- if (total === 0) {
181
- return 0;
182
- }
183
-
184
- return this.stats.hits / total;
185
- }
186
-
187
- /**
188
- * Get cache statistics
189
- *
190
- * @returns {Object} Cache stats
191
- */
192
- getStats() {
193
- return {
194
- ...this.stats,
195
- hitRatio: this.getHitRatio(),
196
- size: this.cache.size,
197
- maxSize: MAX_CACHE_SIZE,
198
- };
199
- }
200
-
201
- /**
202
- * Clear cache
203
- */
204
- clear() {
205
- this.cache.clear();
206
- this.stats = {
207
- hits: 0,
208
- misses: 0,
209
- evictions: 0,
210
- totalSize: 0,
211
- };
212
- }
213
- }
214
-
215
- // Singleton instance
216
- const embeddingCache = new EmbeddingCache();
217
-
218
- module.exports = {
219
- embeddingCache,
220
- EmbeddingCache,
221
- MAX_CACHE_SIZE,
222
- };
@@ -1,141 +0,0 @@
1
- /**
2
- * MAMA Embedding Client
3
- *
4
- * HTTP client for the embedding server running in MCP server.
5
- * Used by hooks for fast embedding generation.
6
- *
7
- * Features:
8
- * - Auto-discovery via port file
9
- * - Timeout handling
10
- * - Fallback to local embedding when server unavailable
11
- *
12
- * @module embedding-client
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const { info, warn } = require('./debug-logger');
18
-
19
- // Configuration
20
- const DEFAULT_PORT = 3847;
21
- const HOST = '127.0.0.1';
22
- const TIMEOUT_MS = 500; // 500ms timeout for fast response
23
- const PORT_FILE = path.join(process.env.HOME || '/tmp', '.mama-embedding-port');
24
-
25
- /**
26
- * Get server port from port file or default
27
- */
28
- function getServerPort() {
29
- try {
30
- if (fs.existsSync(PORT_FILE)) {
31
- const port = parseInt(fs.readFileSync(PORT_FILE, 'utf8').trim(), 10);
32
- if (port > 0 && port < 65536) {
33
- return port;
34
- }
35
- }
36
- } catch (e) {
37
- // Ignore errors, use default
38
- }
39
- return DEFAULT_PORT;
40
- }
41
-
42
- /**
43
- * Check if embedding server is running
44
- *
45
- * @returns {Promise<boolean>} True if server is healthy
46
- */
47
- async function isServerRunning() {
48
- const port = getServerPort();
49
-
50
- try {
51
- const controller = new AbortController();
52
- const timeout = setTimeout(() => controller.abort(), 200);
53
-
54
- const response = await fetch(`http://${HOST}:${port}/health`, {
55
- signal: controller.signal,
56
- });
57
-
58
- clearTimeout(timeout);
59
- return response.ok;
60
- } catch (e) {
61
- return false;
62
- }
63
- }
64
-
65
- /**
66
- * Generate embedding via HTTP server
67
- *
68
- * @param {string} text - Text to embed
69
- * @returns {Promise<Float32Array|null>} Embedding or null if failed
70
- */
71
- async function getEmbeddingFromServer(text) {
72
- const port = getServerPort();
73
-
74
- try {
75
- const controller = new AbortController();
76
- const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
77
-
78
- const response = await fetch(`http://${HOST}:${port}/embed`, {
79
- method: 'POST',
80
- headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify({ text }),
82
- signal: controller.signal,
83
- });
84
-
85
- clearTimeout(timeout);
86
-
87
- if (!response.ok) {
88
- const error = await response.json();
89
- throw new Error(error.error || 'Server error');
90
- }
91
-
92
- const result = await response.json();
93
- info(`[EmbeddingClient] Got embedding in ${result.latency}ms from server`);
94
- return new Float32Array(result.embedding);
95
- } catch (error) {
96
- if (error.name === 'AbortError') {
97
- warn('[EmbeddingClient] Server timeout, will use fallback');
98
- return null;
99
- }
100
- warn(`[EmbeddingClient] Server error: ${error.message}`);
101
- return null;
102
- }
103
- }
104
-
105
- /**
106
- * Get server status
107
- *
108
- * @returns {Promise<Object|null>} Server status or null if not running
109
- */
110
- async function getServerStatus() {
111
- const port = getServerPort();
112
-
113
- try {
114
- const controller = new AbortController();
115
- const timeout = setTimeout(() => controller.abort(), 200);
116
-
117
- const response = await fetch(`http://${HOST}:${port}/health`, {
118
- signal: controller.signal,
119
- });
120
-
121
- clearTimeout(timeout);
122
-
123
- if (response.ok) {
124
- return await response.json();
125
- }
126
- return null;
127
- } catch (e) {
128
- return null;
129
- }
130
- }
131
-
132
- module.exports = {
133
- isServerRunning,
134
- getEmbeddingFromServer,
135
- getServerStatus,
136
- getServerPort,
137
- PORT_FILE,
138
- DEFAULT_PORT,
139
- HOST,
140
- TIMEOUT_MS,
141
- };