/** * RvfLearningStore - Persistent storage for SONA learning artifacts * * Stores patterns, LoRA adapters, EWC state, and trajectories in a * binary-header JSON-lines file format for fast append and rebuild. * * File format: * 4-byte magic "RVLS" + newline * One JSON record per line: {"type":"pattern"|"lora"|"ewc"|"trajectory","data":{...}} * * @module @claude-flow/memory/rvf-learning-store */ import * as fs from 'node:fs'; import * as path from 'node:path'; // ===== Constants ===== const MAGIC_HEADER = 'RVLS'; const DEFAULT_DIMENSIONS = 64; const DEFAULT_AUTO_PERSIST_MS = 30_000; // ===== Helpers ===== function ensureDirectory(filePath) { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } // ===== RvfLearningStore ===== /** * Persistent store for SONA learning artifacts. * * Maintains in-memory maps for fast reads and flushes to a JSON-lines * file with a binary header on persist(). On initialize(), the file is * read line-by-line to rebuild state. * * @example * ```typescript * const store = new RvfLearningStore({ storePath: './data/learning.rvls' }); * await store.initialize(); * * await store.savePatterns([{ id: 'p1', type: 'query_response', ... }]); * await store.persist(); * await store.close(); * ``` */ export class RvfLearningStore { config; patterns = new Map(); loraAdapters = new Map(); ewcState = null; trajectories = []; dirty = false; initialized = false; autoPersistTimer = null; constructor(config) { this.config = { storePath: config.storePath, dimensions: config.dimensions ?? DEFAULT_DIMENSIONS, autoPersistInterval: config.autoPersistInterval ?? DEFAULT_AUTO_PERSIST_MS, verbose: config.verbose ?? false, }; } /** * Initialize the store by loading any existing data from disk. * Creates the parent directory if it does not exist. */ async initialize() { if (this.initialized) return; ensureDirectory(this.config.storePath); if (fs.existsSync(this.config.storePath)) { await this.loadFromDisk(); } if (this.config.autoPersistInterval > 0) { this.autoPersistTimer = setInterval(() => void this.persist().catch(() => { }), this.config.autoPersistInterval); // Allow the process to exit even if the timer is active if (this.autoPersistTimer.unref) { this.autoPersistTimer.unref(); } } this.initialized = true; this.log('Store initialized'); } // ===== Pattern operations ===== /** * Save or update patterns. Existing patterns with matching IDs are * overwritten; new patterns are added. * * @returns The number of patterns stored */ async savePatterns(patterns) { this.ensureInitialized(); let count = 0; for (const pattern of patterns) { this.patterns.set(pattern.id, { ...pattern }); count++; } this.dirty = true; return count; } /** Load all patterns currently held in memory */ async loadPatterns() { this.ensureInitialized(); return Array.from(this.patterns.values()); } /** Return the number of stored patterns */ async getPatternCount() { this.ensureInitialized(); return this.patterns.size; } // ===== LoRA operations ===== /** Save or update a LoRA adapter record */ async saveLoraAdapter(record) { this.ensureInitialized(); this.loraAdapters.set(record.id, { ...record }); this.dirty = true; } /** Load all LoRA adapter records */ async loadLoraAdapters() { this.ensureInitialized(); return Array.from(this.loraAdapters.values()); } /** Delete a LoRA adapter by ID */ async deleteLoraAdapter(id) { this.ensureInitialized(); const existed = this.loraAdapters.delete(id); if (existed) this.dirty = true; return existed; } // ===== EWC operations ===== /** Save EWC state (replaces any existing state) */ async saveEwcState(record) { this.ensureInitialized(); this.ewcState = { ...record }; this.dirty = true; } /** Load the EWC state, or null if none has been stored */ async loadEwcState() { this.ensureInitialized(); return this.ewcState ? { ...this.ewcState } : null; } // ===== Trajectory operations ===== /** Append a trajectory record (append-only, never overwritten) */ async appendTrajectory(record) { this.ensureInitialized(); this.trajectories.push({ ...record }); this.dirty = true; } /** * Return stored trajectories, newest first. * @param limit Maximum number to return (default: all) */ async getTrajectories(limit) { this.ensureInitialized(); const sorted = [...this.trajectories].reverse(); return limit !== undefined ? sorted.slice(0, limit) : sorted; } /** Return the number of stored trajectories */ async getTrajectoryCount() { this.ensureInitialized(); return this.trajectories.length; } // ===== Lifecycle ===== /** * Flush all in-memory state to disk. The entire file is rewritten * to ensure consistency (patterns may have been updated in-place). */ async persist() { if (!this.dirty) return; ensureDirectory(this.config.storePath); const lines = [MAGIC_HEADER]; // Patterns for (const pattern of this.patterns.values()) { lines.push(JSON.stringify({ type: 'pattern', data: pattern })); } // LoRA adapters for (const lora of this.loraAdapters.values()) { lines.push(JSON.stringify({ type: 'lora', data: lora })); } // EWC state if (this.ewcState) { lines.push(JSON.stringify({ type: 'ewc', data: this.ewcState })); } // Trajectories for (const traj of this.trajectories) { lines.push(JSON.stringify({ type: 'trajectory', data: traj })); } const content = lines.join('\n') + '\n'; const tmpPath = this.config.storePath + '.tmp'; await fs.promises.writeFile(tmpPath, content, 'utf-8'); await fs.promises.rename(tmpPath, this.config.storePath); this.dirty = false; this.log(`Persisted: ${this.patterns.size} patterns, ${this.loraAdapters.size} LoRA, ${this.trajectories.length} trajectories`); } /** Persist and release resources */ async close() { if (this.autoPersistTimer) { clearInterval(this.autoPersistTimer); this.autoPersistTimer = null; } if (this.dirty) { await this.persist(); } this.initialized = false; this.log('Store closed'); } // ===== Stats ===== /** Return summary statistics about the store */ async getStats() { this.ensureInitialized(); let fileSizeBytes = 0; try { const stat = await fs.promises.stat(this.config.storePath); fileSizeBytes = stat.size; } catch { // File may not exist yet if nothing has been persisted } return { patterns: this.patterns.size, loraAdapters: this.loraAdapters.size, trajectories: this.trajectories.length, hasEwcState: this.ewcState !== null, fileSizeBytes, }; } // ===== Private ===== async loadFromDisk() { let content; try { content = await fs.promises.readFile(this.config.storePath, 'utf-8'); } catch { return; } const lines = content.split('\n').filter((l) => l.trim().length > 0); if (lines.length === 0) return; // Verify magic header if (lines[0] !== MAGIC_HEADER) { this.log(`Warning: invalid magic header "${lines[0]}", expected "${MAGIC_HEADER}"`); return; } let parsed = 0; let errors = 0; for (let i = 1; i < lines.length; i++) { try { const record = JSON.parse(lines[i]); this.applyRecord(record); parsed++; } catch { errors++; this.log(`Warning: failed to parse line ${i + 1}`); } } this.log(`Loaded from disk: ${parsed} records, ${errors} errors`); } applyRecord(record) { switch (record.type) { case 'pattern': { const p = record.data; this.patterns.set(p.id, p); break; } case 'lora': { const l = record.data; this.loraAdapters.set(l.id, l); break; } case 'ewc': { this.ewcState = record.data; break; } case 'trajectory': { this.trajectories.push(record.data); break; } default: this.log(`Warning: unknown record type "${record.type}"`); } } ensureInitialized() { if (!this.initialized) { throw new Error('RvfLearningStore has not been initialized. Call initialize() first.'); } } log(message) { if (this.config.verbose) { // eslint-disable-next-line no-console console.log(`[RvfLearningStore] ${message}`); } } } //# sourceMappingURL=rvf-learning-store.js.map