infinite-memory
Version:
Infinite context windows for Claude via OpenMemory semantic retrieval
219 lines ⢠9.88 kB
JavaScript
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