tasq/node_modules/@claude-flow/memory/dist/rvf-learning-store.js

295 lines
9.6 KiB
JavaScript

/**
* 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