refactor(01-01): extract memory initialization into src/daemon/memory.ts

- Create initMemory() factory encapsulating MemoryStore, VectorStore, HybridSearch, background indexer, and memory tools registration
- Replace ~65 lines of inline memory init in startDaemon() with single initMemory() call
- Clean up memory-specific imports from daemon/index.ts (MemoryStore, VectorStore, HybridSearch, createEmbeddingProvider, chunkText, contentHash, createMemoryTools)
This commit is contained in:
William Valentin
2026-02-09 20:10:49 -08:00
parent 08f5b6b8e7
commit 00f8f74aac
2 changed files with 102 additions and 74 deletions
+99
View File
@@ -0,0 +1,99 @@
import type { Config } from '../config/index.js';
import type { Lifecycle } from './lifecycle.js';
import { MemoryStore } from '../memory/index.js';
import { VectorStore, HybridSearch, createEmbeddingProvider, chunkText, contentHash } from '../memory/index.js';
import type { EmbeddingProvider as EmbeddingProviderInterface } from '../memory/index.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;
hybridSearch?: HybridSearch;
memoryDir: string;
}
export async function initMemory(deps: MemoryDeps): Promise<MemoryResult> {
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 hybridSearch: HybridSearch | undefined;
if (memoryStore && config.memory.embedding.enabled) {
try {
const embeddingProvider: EmbeddingProviderInterface = createEmbeddingProvider(config.memory.embedding);
const vectorStore = new VectorStore(resolve(dataDir, 'vectors.db'));
hybridSearch = new HybridSearch(
memoryStore,
vectorStore,
embeddingProvider,
config.memory.embedding.hybrid_weight,
);
// 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 (memoryStore) {
for (const tool of createMemoryTools(memoryStore, hybridSearch)) {
toolRegistry.register(tool);
}
}
return { memoryStore, hybridSearch, memoryDir };
}