Skip to main content
Glama
memory-store.ts12.3 kB
/** * Memory store tool - Store or update memories with auto-extraction * v2.0: Merged create + update functionality with summary generation */ import type { DbDriver } from '../database/db-driver.js'; import type { MemoryInput, Memory, Entity, StandardMemory } from '../types/index.js'; import { extractEntities, createEntityInput, deduplicateEntities, } from '../extractors/entity-extractor.js'; import { classifyMemoryType, normalizeContent, validateContent, } from '../extractors/fact-extractor.js'; import { calculateImportance } from '../scoring/importance.js'; import { calculateTTLDays, calculateExpiresAt } from '../scoring/ttl-manager.js'; import { generateId, now, serializeMetadata, deserializeMetadata, } from '../database/connection.js'; import { ValidationError } from '../types/index.js'; import { generateSummary } from '../extractors/summary-generator.js'; import { formatMemory } from './response-formatter.js'; // Database row types (raw, before deserialization) interface EntityRow { id: string; name: string; type: string; metadata: string; created_at: number; } interface MemoryRowDB { id: string; content: string; summary: string; type: string; importance: number; created_at: number; last_accessed: number; access_count: number; expires_at: number | null; metadata: string; is_deleted: number; } /** * Store or update a memory * If input.id is provided, updates existing memory * If input.id is not provided, creates new memory */ export async function memoryStore( db: DbDriver, input: MemoryInput ): Promise<StandardMemory> { // Determine if this is an update or create const isUpdate = !!input.id; if (isUpdate) { return updateMemory(db, input); } else { return createMemory(db, input); } } /** * Create a new memory */ async function createMemory( db: DbDriver, input: MemoryInput ): Promise<StandardMemory> { // Validate content const validation = validateContent(input.content, input.type); if (!validation.valid) { throw new ValidationError(validation.errors.join(', ')); } // Normalize content const normalizedContent = normalizeContent(input.content); // Generate summary const summary = generateSummary(normalizedContent); // Extract entities if not provided let entities = input.entities || []; if (entities.length === 0) { entities = extractEntities(normalizedContent); entities = deduplicateEntities(entities); } // Auto-classify type if needed const finalType = input.type || classifyMemoryType(normalizedContent, entities); // Calculate importance const importance = input.importance ?? calculateImportance( normalizedContent, finalType, entities, input.metadata || {}, input.provenance !== undefined ); // Calculate TTL let expiresAt: number | null = null; if (input.expires_at) { expiresAt = new Date(input.expires_at).getTime(); } else { const ttlDays = input.ttl_days !== undefined ? input.ttl_days : calculateTTLDays(importance); expiresAt = calculateExpiresAt(ttlDays, importance, now()); } // Create memory const memoryId = generateId('mem'); const createdAt = now(); const metadata = input.metadata || {}; // Merge tags into metadata if provided if (input.tags && input.tags.length > 0) { metadata.tags = input.tags; } db.prepare( ` INSERT INTO memories ( id, content, summary, type, importance, embedding, created_at, last_accessed, access_count, expires_at, metadata, is_deleted ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0) ` ).run( memoryId, normalizedContent, summary, finalType, importance, null, // No longer using embeddings createdAt, createdAt, 0, // Initial access_count expiresAt, serializeMetadata(metadata) ); // Create or link entities const entityObjects: Entity[] = []; for (const entityName of entities) { const entityId = createOrGetEntity(db, entityName, normalizedContent); // Fetch entity details const entityRow = db .prepare('SELECT * FROM entities WHERE id = ?') .get(entityId) as EntityRow | undefined; if (entityRow) { entityObjects.push({ id: entityRow.id, name: entityRow.name, type: entityRow.type as Entity['type'], metadata: deserializeMetadata(entityRow.metadata), created_at: entityRow.created_at, }); } // Link memory to entity db.prepare( `INSERT INTO memory_entities (memory_id, entity_id, created_at) VALUES (?, ?, ?)` ).run(memoryId, entityId, createdAt); } // Create provenance record const provenanceId = generateId('prov'); const provenance = input.provenance || { source: 'user', timestamp: new Date().toISOString(), }; db.prepare( ` INSERT INTO provenance ( id, memory_id, operation, timestamp, source, context, user_id, changes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ` ).run( provenanceId, memoryId, 'create', createdAt, provenance.source, provenance.context || null, provenance.user_id || null, null ); // Build Memory object for formatting const memory: Memory = { id: memoryId, content: normalizedContent, summary, type: finalType, importance, created_at: createdAt, last_accessed: createdAt, access_count: 0, expires_at: expiresAt, metadata, is_deleted: false, }; // Format response using standard detail level (NO embeddings) const formattedResponse = formatMemory(memory, 'standard', { entities: entityObjects }) as StandardMemory; return formattedResponse; } /** * Update an existing memory */ async function updateMemory( db: DbDriver, input: MemoryInput ): Promise<StandardMemory> { // Check if memory exists const existing = db .prepare('SELECT * FROM memories WHERE id = ? AND is_deleted = 0') .get(input.id ?? '') as MemoryRowDB | undefined; if (!existing) { throw new ValidationError(`Memory ${input.id} not found or is deleted`); } const changes: Record<string, unknown> = {}; const currentTime = now(); let newContent = existing.content; let newSummary = existing.summary; // Update content if provided if (input.content !== undefined) { newContent = normalizeContent(input.content); newSummary = generateSummary(newContent); db.prepare('UPDATE memories SET content = ?, summary = ? WHERE id = ?').run( newContent, newSummary, input.id ); changes['content'] = { from: existing.content, to: newContent }; changes['summary'] = { from: existing.summary, to: newSummary }; } // Update importance if provided if (input.importance !== undefined) { db.prepare('UPDATE memories SET importance = ? WHERE id = ?').run( input.importance, input.id ); changes['importance'] = { from: existing.importance, to: input.importance, }; } // Update metadata if provided let updatedMetadata = deserializeMetadata(existing.metadata); if (input.metadata !== undefined) { updatedMetadata = { ...updatedMetadata, ...input.metadata }; db.prepare('UPDATE memories SET metadata = ? WHERE id = ?').run( serializeMetadata(updatedMetadata), input.id ); changes['metadata'] = { merged: input.metadata }; } // Update tags in metadata if provided if (input.tags !== undefined) { updatedMetadata.tags = input.tags; db.prepare('UPDATE memories SET metadata = ? WHERE id = ?').run( serializeMetadata(updatedMetadata), input.id ); changes['tags'] = { to: input.tags }; } // Update TTL if provided let newExpiresAt = existing.expires_at; if (input.ttl_days !== undefined || input.expires_at !== undefined) { if (input.expires_at) { newExpiresAt = new Date(input.expires_at).getTime(); } else { const newImportance = input.importance ?? existing.importance; const ttlDays = input.ttl_days ?? calculateTTLDays(newImportance); newExpiresAt = calculateExpiresAt(ttlDays, newImportance, currentTime); } db.prepare('UPDATE memories SET expires_at = ? WHERE id = ?').run( newExpiresAt, input.id ); changes['expires_at'] = { from: existing.expires_at, to: newExpiresAt, }; } // Update entities if provided let entityObjects: Entity[] = []; if (input.entities !== undefined) { // Remove existing entity links db.prepare('DELETE FROM memory_entities WHERE memory_id = ?').run(input.id); // Create new entity links for (const entityName of input.entities) { const entityId = createOrGetEntity(db, entityName, newContent); // Fetch entity details const entityRow = db .prepare('SELECT * FROM entities WHERE id = ?') .get(entityId) as EntityRow | undefined; if (entityRow) { entityObjects.push({ id: entityRow.id, name: entityRow.name, type: entityRow.type as Entity['type'], metadata: deserializeMetadata(entityRow.metadata), created_at: entityRow.created_at, }); } db.prepare( `INSERT INTO memory_entities (memory_id, entity_id, created_at) VALUES (?, ?, ?)` ).run(input.id, entityId, currentTime); } changes['entities'] = { to: input.entities }; } else { // Fetch existing entities const entityRows = db .prepare( ` SELECT e.* FROM entities e JOIN memory_entities me ON e.id = me.entity_id WHERE me.memory_id = ? ` ) .all(input.id) as EntityRow[]; entityObjects = entityRows.map((row) => ({ id: row.id, name: row.name, type: row.type as Entity['type'], metadata: deserializeMetadata(row.metadata), created_at: row.created_at, })); } // Create provenance record const provenanceId = generateId('prov'); const provenance = input.provenance || { source: 'user', }; db.prepare( ` INSERT INTO provenance ( id, memory_id, operation, timestamp, source, context, user_id, changes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ` ).run( provenanceId, input.id, 'update', currentTime, provenance.source, provenance.context || `Updated ${Object.keys(changes).length} field(s)`, provenance.user_id || null, serializeMetadata(changes) ); // Build Memory object for formatting const memory: Memory = { id: input.id ?? existing.id, content: newContent, summary: newSummary, type: existing.type as Memory['type'], importance: input.importance ?? existing.importance, created_at: existing.created_at, last_accessed: existing.last_accessed, access_count: existing.access_count, expires_at: newExpiresAt, metadata: updatedMetadata, is_deleted: false, }; // Format response using standard detail level (NO embeddings) const formattedResponse = formatMemory(memory, 'standard', { entities: entityObjects }) as StandardMemory; return formattedResponse; } /** * Create or get existing entity */ function createOrGetEntity( db: DbDriver, name: string, context: string ): string { // Check if entity exists const existing = db .prepare('SELECT id FROM entities WHERE name = ?') .get(name) as { id: string } | undefined; if (existing) { return existing.id; } // Create new entity const entityInput = createEntityInput(name, context); const entityId = generateId('ent'); db.prepare( ` INSERT INTO entities (id, name, type, metadata, created_at) VALUES (?, ?, ?, ?, ?) ` ).run( entityId, entityInput.name, entityInput.type || 'other', serializeMetadata(entityInput.metadata || {}), now() ); return entityId; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/WhenMoon-afk/claude-memory-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server