claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
611 lines • 23.9 kB
JavaScript
/**
* EWC++ (Elastic Weight Consolidation) Implementation
* Prevents catastrophic forgetting of important patterns during continual learning
*
* Algorithm:
* L_total = L_new + (lambda/2) * sum_i(F_i * (theta_i - theta_old_i)^2)
*
* Where:
* - L_new is the loss on new data
* - lambda is the importance weight (ewcLambda)
* - F_i is the Fisher information for parameter i
* - theta_i is the current parameter value
* - theta_old_i is the previous parameter value
*
* Features:
* - Fisher Information Matrix computation from gradient history
* - Online EWC updates for streaming patterns
* - Selective consolidation based on pattern importance
* - Persistent storage in .swarm/ewc-fisher.json
*
* IMPLEMENTATION NOTE (honesty — see docs/reviews/intelligence-system-audit-2026-05-29.md):
* The penalty math above is real, but `F_i` here is NOT true Fisher information
* (the expectation of squared log-likelihood gradients). There are no model
* gradients in this pattern-memory context, so `F_i` is a HEURISTIC IMPORTANCE
* PROXY: accumulated squared embedding magnitude per dimension
* (`F_i += embedding_i^2`, see computeFisherMatrix). It protects high-magnitude
* embedding dimensions during consolidation — a reasonable importance signal —
* but "Fisher information" overstates it; read `F_i` as "embedding-importance
* weight", not gradient curvature.
*
* @module v3/cli/memory/ewc-consolidation
*/
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// Default Configuration
// ============================================================================
const DEFAULT_EWC_CONFIG = {
lambda: 0.4,
maxPatterns: 1000,
fisherDecayRate: 0.01,
importanceThreshold: 0.3,
storagePath: path.join(process.cwd(), '.swarm', 'ewc-fisher.json'),
onlineMode: true,
dimensions: 384
};
// ============================================================================
// EWC Consolidator Class
// ============================================================================
/**
* EWC++ Consolidator
* Implements Elastic Weight Consolidation with online updates
* for preventing catastrophic forgetting in continual learning
*/
export class EWCConsolidator {
config;
patterns = new Map();
gradientHistory = [];
globalFisher = [];
consolidationHistory = [];
initialized = false;
constructor(config) {
this.config = { ...DEFAULT_EWC_CONFIG, ...config };
this.globalFisher = new Array(this.config.dimensions).fill(0);
}
/**
* Initialize the consolidator by loading persisted state
*/
async initialize() {
if (this.initialized)
return true;
try {
await this.loadFromDisk();
this.initialized = true;
return true;
}
catch {
// Start fresh if no persisted state
this.initialized = true;
return true;
}
}
/**
* Compute Fisher Information Matrix from gradient history
* Uses diagonal approximation for efficiency: F_i = E[g_i^2]
*
* @param patterns - Array of patterns with their gradients/embeddings
* @returns Fisher information diagonal
*/
computeFisherMatrix(patterns) {
const fisher = new Array(this.config.dimensions).fill(0);
let sampleCount = 0;
for (const pattern of patterns) {
if (!pattern.embedding || pattern.embedding.length === 0)
continue;
// Only use successful patterns for Fisher computation
// (we want to preserve what worked)
if (!pattern.success)
continue;
sampleCount++;
// Fisher diagonal is expectation of squared gradients
// For embeddings, we use the embedding values as proxy for gradients
const len = Math.min(pattern.embedding.length, this.config.dimensions);
for (let i = 0; i < len; i++) {
// Accumulate squared values (gradient proxy)
fisher[i] += pattern.embedding[i] * pattern.embedding[i];
}
}
// Normalize by sample count
if (sampleCount > 0) {
for (let i = 0; i < this.config.dimensions; i++) {
fisher[i] /= sampleCount;
}
}
// Update global Fisher with exponential moving average (EWC++)
if (this.config.onlineMode) {
const decay = this.config.fisherDecayRate;
for (let i = 0; i < this.config.dimensions; i++) {
this.globalFisher[i] = (1 - decay) * this.globalFisher[i] + decay * fisher[i];
}
}
return fisher;
}
/**
* Consolidate new patterns with old patterns without forgetting
* Applies EWC penalty to preserve important weights
*
* @param newPatterns - New patterns to incorporate
* @param oldPatterns - Existing patterns to preserve
* @returns Consolidated patterns with modified weights
*/
consolidate(newPatterns, oldPatterns) {
const startTime = performance.now();
const result = {
success: false,
patternsConsolidated: 0,
totalPenalty: 0,
modifiedPatterns: [],
protectedPatterns: [],
duration: 0
};
try {
// Use stored patterns if no old patterns provided
const existingPatterns = oldPatterns || Array.from(this.patterns.values());
// Compute Fisher from successful existing patterns
const fisherInput = existingPatterns
.filter(p => p.successCount > p.failureCount)
.map(p => ({
id: p.id,
embedding: p.weights,
success: true
}));
const fisher = this.computeFisherMatrix(fisherInput);
// Process each new pattern
for (const newPattern of newPatterns) {
if (!newPattern.embedding || newPattern.embedding.length === 0)
continue;
const existingPattern = this.patterns.get(newPattern.id);
if (existingPattern) {
// Calculate EWC penalty for updating existing pattern
const penalty = this.getPenalty(existingPattern.weights, newPattern.embedding, fisher);
// Determine if update is allowed based on penalty
const importanceScore = this.calculateImportance(existingPattern);
if (importanceScore > this.config.importanceThreshold && penalty > this.config.lambda) {
// Protect high-importance patterns with high penalty
result.protectedPatterns.push(newPattern.id);
// Apply constrained update: blend old and new based on importance
const blendFactor = 1 - importanceScore;
const blendedWeights = this.blendWeights(existingPattern.weights, newPattern.embedding, blendFactor, fisher);
existingPattern.weights = blendedWeights;
existingPattern.lastUpdated = Date.now();
result.modifiedPatterns.push(newPattern.id);
}
else {
// Low importance or low penalty: allow full update
existingPattern.weights = newPattern.embedding.slice(0, this.config.dimensions);
existingPattern.lastUpdated = Date.now();
result.modifiedPatterns.push(newPattern.id);
}
// Update Fisher diagonal for this pattern
existingPattern.fisherDiagonal = fisher;
result.totalPenalty += penalty;
}
else {
// New pattern: add directly
const weights = {
id: newPattern.id,
weights: newPattern.embedding.slice(0, this.config.dimensions),
fisherDiagonal: fisher,
importance: 0.5,
successCount: 0,
failureCount: 0,
lastUpdated: Date.now(),
type: newPattern.type,
description: newPattern.description
};
this.patterns.set(newPattern.id, weights);
result.modifiedPatterns.push(newPattern.id);
}
result.patternsConsolidated++;
}
// Prune old patterns if exceeding limit
if (this.patterns.size > this.config.maxPatterns) {
this.pruneOldPatterns();
}
// Record consolidation
this.consolidationHistory.push({
timestamp: Date.now(),
penalty: result.totalPenalty,
patterns: result.patternsConsolidated
});
// Persist to disk
this.saveToDisk();
result.success = true;
result.duration = performance.now() - startTime;
return result;
}
catch (error) {
result.error = error instanceof Error ? error.message : String(error);
result.duration = performance.now() - startTime;
return result;
}
}
/**
* Calculate EWC regularization penalty
*
* L_ewc = (lambda/2) * sum_i(F_i * (theta_i - theta_old_i)^2)
*
* @param oldWeights - Previous weight values
* @param newWeights - New weight values
* @param fisher - Fisher information diagonal (optional, uses global if not provided)
* @returns Regularization penalty value
*/
getPenalty(oldWeights, newWeights, fisher) {
const fisherDiag = fisher || this.globalFisher;
const len = Math.min(oldWeights.length, newWeights.length, fisherDiag.length);
let penalty = 0;
for (let i = 0; i < len; i++) {
const diff = newWeights[i] - oldWeights[i];
penalty += fisherDiag[i] * diff * diff;
}
return (this.config.lambda / 2) * penalty;
}
/**
* Get consolidation statistics
*/
getConsolidationStats() {
let totalFisher = 0;
let maxFisher = 0;
let highImportance = 0;
for (let i = 0; i < this.globalFisher.length; i++) {
totalFisher += this.globalFisher[i];
if (this.globalFisher[i] > maxFisher) {
maxFisher = this.globalFisher[i];
}
}
for (const pattern of this.patterns.values()) {
if (this.calculateImportance(pattern) > this.config.importanceThreshold) {
highImportance++;
}
}
const totalPenalty = this.consolidationHistory.reduce((sum, h) => sum + h.penalty, 0);
const avgPenalty = this.consolidationHistory.length > 0
? totalPenalty / this.consolidationHistory.length
: 0;
// Estimate storage size
let storageSizeBytes = 0;
try {
if (fs.existsSync(this.config.storagePath)) {
const stats = fs.statSync(this.config.storagePath);
storageSizeBytes = stats.size;
}
}
catch {
// Ignore stat errors
}
return {
totalPatterns: this.patterns.size,
highImportancePatterns: highImportance,
avgFisherValue: this.globalFisher.length > 0 ? totalFisher / this.globalFisher.length : 0,
maxFisherValue: maxFisher,
consolidationCount: this.consolidationHistory.length,
lastConsolidation: this.consolidationHistory.length > 0
? this.consolidationHistory[this.consolidationHistory.length - 1].timestamp
: null,
avgPenalty,
storageSizeBytes
};
}
/**
* Record a gradient sample for Fisher computation
*/
recordGradient(patternId, gradients, success) {
this.gradientHistory.push({
patternId,
gradients,
timestamp: Date.now(),
success
});
// Keep only recent gradients
const maxGradients = this.config.maxPatterns * 2;
if (this.gradientHistory.length > maxGradients) {
this.gradientHistory = this.gradientHistory.slice(-maxGradients);
}
// Update pattern success/failure counts
const pattern = this.patterns.get(patternId);
if (pattern) {
if (success) {
pattern.successCount++;
}
else {
pattern.failureCount++;
}
pattern.importance = this.calculateImportance(pattern);
}
// Online Fisher update from this gradient
if (this.config.onlineMode && success) {
const decay = this.config.fisherDecayRate;
const len = Math.min(gradients.length, this.config.dimensions);
for (let i = 0; i < len; i++) {
this.globalFisher[i] = (1 - decay) * this.globalFisher[i] + decay * gradients[i] * gradients[i];
}
}
}
/**
* Get pattern weights by ID
*/
getPatternWeights(id) {
return this.patterns.get(id);
}
/**
* Get all stored patterns
*/
getAllPatterns() {
return Array.from(this.patterns.values());
}
/**
* Update EWC lambda (regularization strength)
*/
setLambda(lambda) {
this.config.lambda = lambda;
}
/**
* Get current lambda value
*/
getLambda() {
return this.config.lambda;
}
/**
* Reset Fisher matrix (use with caution - allows forgetting)
*/
resetFisher() {
this.globalFisher = new Array(this.config.dimensions).fill(0);
}
/**
* Update Fisher matrix from pattern confidence changes.
* Called by SONA after distillLearning to track which patterns
* are important and should be protected from forgetting.
*
* Uses online averaging: F_new = alpha * F_old + (1-alpha) * F_current
*
* @param confidenceChanges - Array of {id, embedding, oldConf, newConf}
*/
updateFisherFromConfidences(confidenceChanges) {
if (confidenceChanges.length === 0)
return;
const alpha = this.config.fisherDecayRate;
const currentFisher = new Array(this.config.dimensions).fill(0);
let sampleCount = 0;
for (const change of confidenceChanges) {
if (!change.embedding || change.embedding.length === 0)
continue;
const confDelta = Math.abs(change.newConf - change.oldConf);
if (confDelta === 0)
continue;
sampleCount++;
const len = Math.min(change.embedding.length, this.config.dimensions);
// Squared gradient proxy: embedding scaled by confidence change magnitude
for (let i = 0; i < len; i++) {
const grad = change.embedding[i] * confDelta;
currentFisher[i] += grad * grad;
}
}
if (sampleCount > 0) {
for (let i = 0; i < this.config.dimensions; i++) {
currentFisher[i] /= sampleCount;
}
}
// Online EMA: F_new = alpha * F_old + (1-alpha) * F_current
for (let i = 0; i < this.config.dimensions; i++) {
this.globalFisher[i] = alpha * this.globalFisher[i] + (1 - alpha) * currentFisher[i];
}
this.saveToDisk();
}
/**
* Compute consolidation penalty for a proposed confidence update.
* Used by SONA to check whether a pattern update would cause forgetting.
*
* @param oldConfidence - Current confidence value
* @param newConfidence - Proposed new confidence value
* @returns Penalty value (higher = more forgetting risk)
*/
computeConfidencePenalty(oldConfidence, newConfidence) {
// Use the global Fisher to estimate penalty for scalar confidence change
// Average Fisher value represents overall importance
let avgFisher = 0;
for (let i = 0; i < this.globalFisher.length; i++) {
avgFisher += this.globalFisher[i];
}
avgFisher = this.globalFisher.length > 0 ? avgFisher / this.globalFisher.length : 0;
const diff = newConfidence - oldConfidence;
return (this.config.lambda / 2) * avgFisher * diff * diff;
}
/**
* Clear all patterns and history (full reset)
*/
clear() {
this.patterns.clear();
this.gradientHistory = [];
this.globalFisher = new Array(this.config.dimensions).fill(0);
this.consolidationHistory = [];
// Remove persisted file
try {
if (fs.existsSync(this.config.storagePath)) {
fs.unlinkSync(this.config.storagePath);
}
}
catch {
// Ignore deletion errors
}
}
// ============================================================================
// Private Methods
// ============================================================================
/**
* Calculate importance score for a pattern based on usage
*/
calculateImportance(pattern) {
const total = pattern.successCount + pattern.failureCount;
if (total === 0)
return 0.5;
// Success rate with Laplace smoothing
const successRate = (pattern.successCount + 1) / (total + 2);
// Recency factor: recent patterns are more important
const hoursSinceUpdate = (Date.now() - pattern.lastUpdated) / (1000 * 60 * 60);
const recencyFactor = Math.exp(-hoursSinceUpdate / 168); // 1 week half-life
// Combine factors
return successRate * 0.7 + recencyFactor * 0.3;
}
/**
* Blend old and new weights using Fisher-weighted interpolation
*/
blendWeights(oldWeights, newWeights, blendFactor, fisher) {
const len = Math.min(oldWeights.length, newWeights.length, this.config.dimensions);
const result = new Array(len);
// Normalize Fisher for per-weight blend factors
let maxF = 0;
for (let i = 0; i < len; i++) {
if (fisher[i] > maxF)
maxF = fisher[i];
}
const normFactor = maxF > 0 ? 1 / maxF : 1;
for (let i = 0; i < len; i++) {
// Higher Fisher = more weight on old value
const fisherWeight = fisher[i] * normFactor;
const adjustedBlend = blendFactor * (1 - fisherWeight * 0.5);
result[i] = oldWeights[i] * (1 - adjustedBlend) + newWeights[i] * adjustedBlend;
}
return result;
}
/**
* Prune old, low-importance patterns to stay within limit
*/
pruneOldPatterns() {
if (this.patterns.size <= this.config.maxPatterns)
return;
// Sort by importance (ascending)
const sortedPatterns = Array.from(this.patterns.entries())
.map(([id, pattern]) => ({ id, importance: this.calculateImportance(pattern) }))
.sort((a, b) => a.importance - b.importance);
// Remove lowest importance patterns
const toRemove = this.patterns.size - this.config.maxPatterns;
for (let i = 0; i < toRemove; i++) {
this.patterns.delete(sortedPatterns[i].id);
}
}
/**
* Save state to disk
*/
saveToDisk() {
try {
const dir = path.dirname(this.config.storagePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const state = {
version: '1.0.0',
config: {
lambda: this.config.lambda,
dimensions: this.config.dimensions,
fisherDecayRate: this.config.fisherDecayRate
},
globalFisher: this.globalFisher,
patterns: Array.from(this.patterns.entries()),
consolidationHistory: this.consolidationHistory.slice(-100),
savedAt: Date.now()
};
fs.writeFileSync(this.config.storagePath, JSON.stringify(state, null, 2));
}
catch {
// Silently fail - persistence is best-effort
}
}
/**
* Load state from disk
*/
async loadFromDisk() {
if (!fs.existsSync(this.config.storagePath)) {
throw new Error('No persisted state found');
}
const content = fs.readFileSync(this.config.storagePath, 'utf-8');
const state = JSON.parse(content);
// Validate version
if (state.version !== '1.0.0') {
throw new Error(`Unsupported state version: ${state.version}`);
}
// Restore state
this.globalFisher = state.globalFisher || new Array(this.config.dimensions).fill(0);
// Restore patterns
this.patterns.clear();
if (state.patterns) {
for (const [id, pattern] of state.patterns) {
this.patterns.set(id, pattern);
}
}
// Restore history
this.consolidationHistory = state.consolidationHistory || [];
// Update config from persisted values
if (state.config) {
this.config.lambda = state.config.lambda ?? this.config.lambda;
}
}
}
// ============================================================================
// Singleton Instance
// ============================================================================
let ewcConsolidatorInstance = null;
/**
* Get the singleton EWC Consolidator instance
*
* @param config - Optional configuration overrides
* @returns EWC Consolidator instance
*/
export async function getEWCConsolidator(config) {
if (!ewcConsolidatorInstance) {
ewcConsolidatorInstance = new EWCConsolidator(config);
await ewcConsolidatorInstance.initialize();
}
return ewcConsolidatorInstance;
}
/**
* Reset the singleton instance (for testing)
*/
export function resetEWCConsolidator() {
if (ewcConsolidatorInstance) {
ewcConsolidatorInstance.clear();
ewcConsolidatorInstance = null;
}
}
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Quick consolidation helper for common use case
* Consolidates new patterns with existing ones using EWC
*
* @param newPatterns - New patterns to add
* @returns Consolidation result
*/
export async function consolidatePatterns(newPatterns) {
const consolidator = await getEWCConsolidator();
return consolidator.consolidate(newPatterns);
}
/**
* Record pattern usage outcome
* Updates Fisher information and pattern importance
*
* @param patternId - Pattern identifier
* @param embedding - Pattern embedding (used as gradient proxy)
* @param success - Whether the pattern was successful
*/
export async function recordPatternOutcome(patternId, embedding, success) {
const consolidator = await getEWCConsolidator();
consolidator.recordGradient(patternId, embedding, success);
}
/**
* Get EWC statistics
*/
export async function getEWCStats() {
const consolidator = await getEWCConsolidator();
return consolidator.getConsolidationStats();
}
export default {
EWCConsolidator,
getEWCConsolidator,
resetEWCConsolidator,
consolidatePatterns,
recordPatternOutcome,
getEWCStats
};
//# sourceMappingURL=ewc-consolidation.js.map