tasq/node_modules/@claude-flow/memory/dist/persistent-sona.js

332 lines
12 KiB
JavaScript

/**
* PersistentSonaCoordinator - SONA learning with RVF persistence
*
* Wraps RvfLearningStore to provide an in-memory pattern bank with
* brute-force cosine similarity, trajectory buffering, EWC tracking,
* and automatic periodic persistence to disk.
*
* This is intentionally decoupled from the ruvector SONA classes:
* it defines its own compatible types and delegates persistence to
* RvfLearningStore.
*
* @module @claude-flow/memory/persistent-sona
*/
import { RvfLearningStore, } from './rvf-learning-store.js';
// ===== Constants =====
const DEFAULT_PATTERN_THRESHOLD = 0.85;
const DEFAULT_MAX_TRAJECTORY_BUFFER = 1000;
const DEFAULT_AUTO_PERSIST_MS = 30_000;
// ===== Helpers =====
/**
* Compute cosine similarity between two number arrays.
* Returns 0 when either vector has zero magnitude.
*/
function cosineSimilarity(a, b) {
const len = Math.min(a.length, b.length);
let dot = 0;
let normA = 0;
let normB = 0;
// Process in groups of 4 for better throughput
let i = 0;
for (; i + 3 < len; i += 4) {
dot += a[i] * b[i] + a[i + 1] * b[i + 1] + a[i + 2] * b[i + 2] + a[i + 3] * b[i + 3];
normA += a[i] * a[i] + a[i + 1] * a[i + 1] + a[i + 2] * a[i + 2] + a[i + 3] * a[i + 3];
normB += b[i] * b[i] + b[i + 1] * b[i + 1] + b[i + 2] * b[i + 2] + b[i + 3] * b[i + 3];
}
for (; i < len; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom > 0 ? dot / denom : 0;
}
function generateId(prefix) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===== PersistentSonaCoordinator =====
/**
* Coordinates SONA learning with persistent storage.
*
* @example
* ```typescript
* const sona = new PersistentSonaCoordinator({
* storePath: './data/sona-learning.rvls',
* });
* await sona.initialize();
*
* // Store a pattern
* const id = sona.storePattern('query_response', embedding);
*
* // Find similar patterns
* const matches = sona.findSimilarPatterns(queryEmbedding, 5);
*
* // Record a trajectory
* sona.recordTrajectory({ id: 'traj-1', steps: [...], outcome: 'success', ... });
*
* // Periodic background learning
* const result = sona.runBackgroundLoop();
*
* await sona.shutdown();
* ```
*/
export class PersistentSonaCoordinator {
store;
patterns = new Map();
trajectoryBuffer = [];
ewcState = null;
patternThreshold;
maxTrajectoryBuffer;
verbose;
initialized = false;
constructor(config) {
this.patternThreshold = config.patternThreshold ?? DEFAULT_PATTERN_THRESHOLD;
this.maxTrajectoryBuffer = config.maxTrajectoryBuffer ?? DEFAULT_MAX_TRAJECTORY_BUFFER;
this.verbose = config.verbose ?? false;
const storeConfig = {
storePath: config.storePath,
autoPersistInterval: config.autoPersistInterval ?? DEFAULT_AUTO_PERSIST_MS,
verbose: config.verbose,
};
this.store = new RvfLearningStore(storeConfig);
}
/**
* Initialize by loading persisted state from the RVF store.
*/
async initialize() {
if (this.initialized)
return;
await this.store.initialize();
// Rebuild in-memory state from store
const patterns = await this.store.loadPatterns();
for (const p of patterns) {
this.patterns.set(p.id, p);
}
const ewc = await this.store.loadEwcState();
this.ewcState = ewc;
const trajectories = await this.store.getTrajectories();
// getTrajectories returns newest-first, reverse for chronological buffer
this.trajectoryBuffer = trajectories.reverse();
this.initialized = true;
this.log(`Initialized: ${this.patterns.size} patterns, ` +
`${this.trajectoryBuffer.length} trajectories, ` +
`EWC: ${this.ewcState ? 'yes' : 'no'}`);
}
// ===== Pattern operations =====
/**
* Store a new pattern and return its ID.
*
* @param type - Pattern type (e.g. 'query_response', 'routing')
* @param embedding - The pattern embedding vector
* @param metadata - Optional extra metadata (currently unused, reserved)
* @returns The generated pattern ID
*/
storePattern(type, embedding, metadata) {
this.ensureInitialized();
const id = generateId('pat');
const record = {
id,
type,
embedding: [...embedding],
successRate: 1.0,
useCount: 0,
lastUsed: new Date().toISOString(),
};
this.patterns.set(id, record);
// Mark for persistence on next persist() call
void this.store.savePatterns([record]).catch(() => { });
return id;
}
/**
* Find the k most similar patterns above the configured threshold.
* Uses brute-force cosine similarity (suitable for small pattern sets).
*/
findSimilarPatterns(embedding, k = 5) {
this.ensureInitialized();
const results = [];
for (const record of this.patterns.values()) {
const score = cosineSimilarity(embedding, record.embedding);
if (score >= this.patternThreshold) {
results.push({ record, score });
}
}
// Sort descending by score and take top-k
results.sort((a, b) => b.score - a.score);
return results.slice(0, k).map((r) => r.record);
}
/**
* Record a pattern usage outcome. Updates the success rate using an
* exponential moving average (alpha = 0.1).
*/
recordPatternUsage(patternId, success) {
this.ensureInitialized();
const pattern = this.patterns.get(patternId);
if (!pattern)
return;
pattern.useCount++;
pattern.lastUsed = new Date().toISOString();
const alpha = 0.1;
const outcome = success ? 1.0 : 0.0;
pattern.successRate = alpha * outcome + (1 - alpha) * pattern.successRate;
}
/**
* Remove patterns that have low success rates after sufficient usage.
*
* @returns The number of patterns pruned
*/
prunePatterns(minSuccessRate = 0.3, minUseCount = 5) {
this.ensureInitialized();
let pruned = 0;
for (const [id, pattern] of this.patterns) {
if (pattern.useCount >= minUseCount && pattern.successRate < minSuccessRate) {
this.patterns.delete(id);
pruned++;
}
}
if (pruned > 0) {
this.log(`Pruned ${pruned} low-performing patterns`);
}
return pruned;
}
// ===== Trajectory tracking =====
/**
* Buffer a completed trajectory for later processing.
* When the buffer exceeds maxTrajectoryBuffer, the oldest entries
* are evicted.
*/
recordTrajectory(trajectory) {
this.ensureInitialized();
const copy = { ...trajectory };
this.trajectoryBuffer.push(copy);
// Persist to store immediately so evicted entries are not lost
void this.store.appendTrajectory(copy).catch(() => { });
while (this.trajectoryBuffer.length > this.maxTrajectoryBuffer) {
this.trajectoryBuffer.shift();
}
}
// ===== Learning loop =====
/**
* Process buffered trajectories to extract new patterns.
* Successful and partial trajectories are mined for high-confidence
* steps; new patterns are stored if they are sufficiently different
* from existing ones.
*
* After processing, the trajectory buffer is cleared and low-performing
* patterns are pruned.
*
* @returns Summary of the learning pass
*/
runBackgroundLoop() {
this.ensureInitialized();
let patternsLearned = 0;
const trajectoriesProcessed = this.trajectoryBuffer.length;
for (const traj of this.trajectoryBuffer) {
if (traj.outcome === 'success' || traj.outcome === 'partial') {
patternsLearned += this.extractPatternsFromTrajectory(traj);
}
}
this.prunePatterns();
this.trajectoryBuffer = [];
this.log(`Background loop: ${patternsLearned} patterns learned, ` +
`${trajectoriesProcessed} trajectories processed`);
return { patternsLearned, trajectoriesProcessed };
}
// ===== Persistence =====
/**
* Flush current in-memory state to the RVF store on disk.
*/
async persist() {
const allPatterns = Array.from(this.patterns.values());
await this.store.savePatterns(allPatterns);
if (this.ewcState) {
await this.store.saveEwcState(this.ewcState);
}
// Persist any buffered trajectories that have not yet been saved
for (const traj of this.trajectoryBuffer) {
await this.store.appendTrajectory(traj);
}
await this.store.persist();
}
/**
* Persist state and shut down the store.
*/
async shutdown() {
if (!this.initialized)
return;
await this.persist();
await this.store.close();
this.initialized = false;
this.log('Shutdown complete');
}
// ===== Stats =====
/**
* Return a summary of the coordinator's current state.
*/
getStats() {
let totalSuccessRate = 0;
let count = 0;
for (const p of this.patterns.values()) {
totalSuccessRate += p.successRate;
count++;
}
return {
patterns: this.patterns.size,
avgSuccessRate: count > 0 ? totalSuccessRate / count : 0,
trajectoriesBuffered: this.trajectoryBuffer.length,
ewcTasksLearned: this.ewcState?.tasksLearned ?? 0,
};
}
// ===== Private =====
/**
* Extract patterns from a trajectory's high-confidence steps.
* A step produces a new pattern only if no sufficiently similar
* pattern already exists.
*/
extractPatternsFromTrajectory(trajectory) {
let extracted = 0;
for (const step of trajectory.steps) {
if (step.confidence < this.patternThreshold)
continue;
const embedding = this.createHashEmbedding(step.input + step.output);
const similar = this.findSimilarPatterns(embedding, 1);
if (similar.length === 0) {
this.storePattern(step.type, embedding);
extracted++;
}
}
return extracted;
}
/**
* Deterministic hash-based embedding for pattern extraction.
* This is a lightweight stand-in for a real embedding model,
* matching the approach used in SonaCoordinator.
*/
createHashEmbedding(text, dim = 64) {
const embedding = new Array(dim).fill(0);
for (let i = 0; i < text.length; i++) {
const idx = (text.charCodeAt(i) * (i + 1)) % dim;
embedding[idx] += 0.1;
}
let norm = 0;
for (let i = 0; i < dim; i++) {
norm += embedding[i] * embedding[i];
}
norm = Math.sqrt(norm) || 1;
for (let i = 0; i < dim; i++) {
embedding[i] /= norm;
}
return embedding;
}
ensureInitialized() {
if (!this.initialized) {
throw new Error('PersistentSonaCoordinator has not been initialized. Call initialize() first.');
}
}
log(message) {
if (this.verbose) {
// eslint-disable-next-line no-console
console.log(`[PersistentSona] ${message}`);
}
}
}
//# sourceMappingURL=persistent-sona.js.map