UNPKG

infinite-memory

Version:

Infinite context windows for Claude via OpenMemory semantic retrieval

219 lines • 9.88 kB
import { OpenMemory } from 'openmemory-js'; import { extractSearchableText } from './utils/messageFormatter.js'; export class OpenMemoryClient { client; timeout; constructor(config) { this.client = new OpenMemory({ baseUrl: config.baseUrl, apiKey: config.apiKey, }); this.timeout = config.timeout || 2000; } chunkText(text, maxChunkSize = 100000) { const chunks = []; for (let i = 0; i < text.length; i += maxChunkSize) { chunks.push(text.substring(i, i + maxChunkSize)); } return chunks; } async addMessage(message) { try { const searchableText = extractSearchableText(message); if (!searchableText || searchableText.trim().length === 0) { console.error(`āŒ [InfiniteMemory] Cannot store message ${message.id}: Empty content`, { role: message.role, contentType: typeof message.content, hasPartsArray: !!message.content?.parts, partsLength: message.content?.parts?.length, extractedText: searchableText, }); return; } console.log(`šŸ” [InfiniteMemory] Storing message ${message.id}:`, { role: message.role, textLength: searchableText.length, textPreview: searchableText.substring(0, 100), }); const MAX_SIZE = 200000; if (searchableText.length > MAX_SIZE) { const chunks = this.chunkText(searchableText, 100000); console.log(`šŸ“¦ [InfiniteMemory] Message too large, storing in ${chunks.length} chunks (parallel)`); const startTime = Date.now(); await Promise.all(chunks.map((chunk, i) => { const chunkId = `${message.id}-chunk-${i + 1}`; console.log(`šŸ“ [InfiniteMemory] Storing chunk ${i + 1}/${chunks.length}: ${chunkId}`); return this.client.add(chunk, { user_id: `${message.userId}-${message.role}`, tags: [ 'message', 'chunk', message.role, message.conversationId, message.id, ], metadata: { timestamp: message.timestamp, messageId: message.id, chunkId, chunkIndex: i + 1, totalChunks: chunks.length, role: message.role, userId: message.userId, conversationId: message.conversationId, }, }); })); const duration = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`āœ… [InfiniteMemory] Stored message ${message.id} in ${chunks.length} chunks (${duration}s)`); return; } const result = await this.client.add(searchableText, { user_id: `${message.userId}-${message.role}`, tags: [ 'message', message.role, message.conversationId, message.id, ], metadata: { timestamp: message.timestamp, messageId: message.id, role: message.role, userId: message.userId, conversationId: message.conversationId, }, }); console.log(`šŸ“ [InfiniteMemory] Stored message ${message.id} (${message.role})`, result); } catch (error) { console.error(`āŒ [InfiniteMemory] Failed to store message ${message.id}:`, error); console.error(`āŒ [InfiniteMemory] Message details:`, { role: message.role, contentType: typeof message.content, contentLength: JSON.stringify(message.content).length, }); } } async checkMessagesExist(userId, messageIds) { const foundIds = new Set(); for (const id of messageIds) { try { const result = await this.client.query('', { k: 1, filters: { user_id: `${userId}-user`, tags: [id], } }); if (result.matches.length > 0) { foundIds.add(id); } } catch (error) { continue; } } return foundIds; } async queryRelevant(conversationId, userId, queryText, k = 20) { const kPerRole = Math.ceil(k / 2); const [userResults, assistantResults] = await Promise.all([ this.queryByRole(conversationId, userId, 'user', queryText, kPerRole), this.queryByRole(conversationId, userId, 'assistant', queryText, kPerRole), ]); return { userMemories: userResults, assistantMemories: assistantResults, }; } async queryByRole(conversationId, userId, role, queryText, k) { try { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('OpenMemory query timeout')), this.timeout); }); const userIdWithRole = `${userId}-${role}`; const result = await Promise.race([ this.client.query(queryText, { k, filters: { user_id: userIdWithRole, tags: [conversationId], } }), timeoutPromise, ]); console.log(`šŸ” [InfiniteMemory] Found ${result.matches.length} ${role} memories (filtered by user_id: ${userIdWithRole}, tags: [${conversationId}])`); if (result.matches.length > 0) { console.log(`\nšŸ” [InfiniteMemory] Verifying ${role} memory isolation...`); console.log('─'.repeat(80)); let perfectMatches = 0; let crossConversationMatches = 0; let legacyMatches = 0; result.matches.forEach((match, idx) => { const metadata = match.metadata || {}; const tags = match.tags || []; const matchUserId = metadata.userId; const matchConversationId = metadata.conversationId; const hasConversationTag = tags.includes(conversationId); const hasMissingMetadata = !matchUserId && tags.length === 0; let status; if (hasMissingMetadata) { status = 'šŸ”µ'; legacyMatches++; } else { const userMatch = matchUserId === userId; const convMatch = hasConversationTag || matchConversationId === conversationId; if (userMatch && convMatch) { status = 'āœ…'; perfectMatches++; } else if (userMatch && !convMatch) { status = 'āš ļø'; crossConversationMatches++; } else { status = 'āŒ'; perfectMatches++; } } if (idx < 5) { console.log(`${status} ${role} memory ${idx + 1}: "${match.content.substring(0, 60)}..."`); } }); if (result.matches.length > 5) { console.log(`... (${result.matches.length - 5} more ${role} memories)`); } console.log('─'.repeat(80)); console.log(`šŸ“Š ${role} memory breakdown:`); if (perfectMatches > 0) { console.log(` āœ… Same user + Same conversation: ${perfectMatches}`); } if (crossConversationMatches > 0) { console.log(` āš ļø Same user + Different conversation: ${crossConversationMatches}`); } if (legacyMatches > 0) { console.log(` šŸ”µ Legacy (no metadata, trusting user_id filter): ${legacyMatches}`); } if (legacyMatches === result.matches.length) { console.log(`ā„¹ļø All ${role} memories are legacy. OpenMemory filtered by user_id=${userIdWithRole}, so these should be isolated.`); } else if (perfectMatches + crossConversationMatches + legacyMatches === result.matches.length) { console.log(`āœ… ${role} memories verified - all belong to user ${userId.substring(0, 8)}...`); } console.log(''); } return result.matches.map((match) => ({ content: match.content, score: match.score, timestamp: match.last_seen_at, })); } catch (error) { console.error(`āš ļø [InfiniteMemory] OpenMemory query for ${role} failed (${error instanceof Error ? error.message : 'unknown'}), will use fallback`); return []; } } } //# sourceMappingURL=OpenMemoryClient.js.map