import type { Config } from '../config/index.js'; import type { Lifecycle } from './lifecycle.js'; import { MemoryStore } from '../memory/index.js'; import { VectorStore, HybridSearch, QmdSearch, createEmbeddingProvider, chunkText, contentHash } from '../memory/index.js'; import type { EmbeddingProvider as EmbeddingProviderInterface } from '../memory/index.js'; import type { MemorySearchBackend } from '../tools/builtin/memory-search.js'; import { createMemoryTools } from '../tools/builtin/index.js'; import type { ToolRegistry } from '../tools/index.js'; import { resolve } from 'path'; import { mkdirSync } from 'fs'; export interface MemoryDeps { config: Config; dataDir: string; lifecycle: Lifecycle; toolRegistry: ToolRegistry; } export interface MemoryResult { memoryStore?: MemoryStore; searchBackend?: MemorySearchBackend; memoryDir: string; } export async function initMemory(deps: MemoryDeps): Promise { const { config, dataDir, lifecycle, toolRegistry } = deps; // Initialize memory store const memoryDir = config.memory.dir ?? resolve(dataDir, 'memory'); mkdirSync(memoryDir, { recursive: true }); const memoryStore = config.memory.enabled ? new MemoryStore({ dir: memoryDir, maxContextTokens: config.memory.max_context_tokens }) : undefined; // Register memory tools if memory is enabled let searchBackend: MemorySearchBackend | undefined; if (memoryStore && config.memory.embedding.enabled) { try { const embeddingProvider: EmbeddingProviderInterface = createEmbeddingProvider(config.memory.embedding); const vectorStore = new VectorStore(resolve(dataDir, 'vectors.db')); const hybridSearch = new HybridSearch( memoryStore, vectorStore, embeddingProvider, config.memory.embedding.hybrid_weight, ); searchBackend = hybridSearch; // Background indexer: re-embed dirty namespaces every 30 seconds const indexerInterval = setInterval(async () => { const dirtyNamespaces = memoryStore.getDirtyNamespaces(); for (const ns of dirtyNamespaces) { try { const content = memoryStore.read(ns); if (content.length === 0) { vectorStore.deleteNamespace(ns); continue; } const hash = contentHash(content); if (vectorStore.hasContentHash(ns, hash)) {continue;} const chunks = chunkText(content, ns, { chunkSize: config.memory.embedding.chunk_size, chunkOverlap: config.memory.embedding.chunk_overlap, }); if (chunks.length > 0) { const embeddings = await embeddingProvider.embed(chunks.map((c) => c.text)); vectorStore.upsertChunks(chunks, embeddings, hash); } } catch (err) { console.error(`Failed to index namespace "${ns}":`, err); } } }, 30_000); // Initial full index — mark all existing namespaces as dirty memoryStore.markAllDirty(); lifecycle.onShutdown(async () => { clearInterval(indexerInterval); vectorStore.close(); console.log('Vector store closed'); }); console.log(`Vector memory search enabled (provider=${config.memory.embedding.provider}, model=${config.memory.embedding.model})`); } catch (err) { console.error('Failed to initialize vector search:', err); } } if (!searchBackend && memoryStore && config.memory.qmd.enabled) { searchBackend = new QmdSearch(memoryStore, { topK: config.memory.qmd.top_k, minScore: config.memory.qmd.min_score, }); console.log(`QMD memory search enabled (top_k=${config.memory.qmd.top_k}, min_score=${config.memory.qmd.min_score})`); } if (memoryStore) { for (const tool of createMemoryTools(memoryStore, searchBackend)) { toolRegistry.register(tool); } } return { memoryStore, searchBackend, memoryDir }; }