mega-minds
Version:
Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration
456 lines (385 loc) • 16.6 kB
JavaScript
// lib/utils/ContextCompressor.js
const fs = require('fs-extra');
const path = require('path');
/**
* ContextCompressor - Intelligent context compression and summarization
* Keeps essential project information while reducing token usage for Claude Code
*/
class ContextCompressor {
constructor(projectPath) {
this.projectPath = projectPath;
this.memoryPath = path.join(projectPath, '.mega-minds');
this.compressionSettings = {
tokenLimit: 200000,
compressionThreshold: 160000, // 80% of limit
aggressiveThreshold: 180000, // 90% of limit
preserveRatio: {
permanent: 1.0, // Never compress
critical: 0.7, // Compress to 70%
important: 0.4, // Compress to 40%
historical: 0.1, // Compress to 10%
temporary: 0.05 // Aggressive compression
}
};
}
/**
* Analyze content and determine if compression is needed
*/
analyzeContent(content) {
const tokenCount = this.estimateTokenCount(content);
const compressionNeeded = tokenCount > this.compressionSettings.compressionThreshold;
const aggressiveNeeded = tokenCount > this.compressionSettings.aggressiveThreshold;
return {
tokenCount,
compressionNeeded,
aggressiveNeeded,
compressionRatio: compressionNeeded ?
(aggressiveNeeded ? 0.6 : 0.8) : 1.0
};
}
/**
* Compress context intelligently based on content type and importance
*/
async compressContext(contextData) {
const analysis = this.analyzeContent(JSON.stringify(contextData));
if (!analysis.compressionNeeded) {
return {
compressed: contextData,
compressionApplied: false,
originalTokens: analysis.tokenCount,
compressedTokens: analysis.tokenCount,
compressionRatio: 1.0
};
}
console.log(`🗜️ Compressing context: ${analysis.tokenCount} tokens → target: ${Math.round(analysis.tokenCount * analysis.compressionRatio)}`);
// Categorize content by importance
const categorized = this.categorizeContent(contextData);
// Apply compression strategies
const compressed = {
permanent: categorized.permanent, // Never compress
critical: await this.compressCritical(categorized.critical, analysis.aggressiveNeeded),
important: await this.compressImportant(categorized.important, analysis.aggressiveNeeded),
historical: await this.compressHistorical(categorized.historical),
summary: await this.generateContextSummary(contextData)
};
const compressedTokens = this.estimateTokenCount(JSON.stringify(compressed));
return {
compressed,
compressionApplied: true,
originalTokens: analysis.tokenCount,
compressedTokens,
compressionRatio: compressedTokens / analysis.tokenCount,
compressionReport: this.generateCompressionReport(categorized, compressed)
};
}
/**
* Categorize content by importance level
*/
categorizeContent(contextData) {
const categorized = {
permanent: {},
critical: {},
important: {},
historical: {},
temporary: {}
};
// Permanent content (never compress)
if (contextData.project) {
categorized.permanent.architecture = contextData.project.architecture;
categorized.permanent.coreDecisions = contextData.project.coreDecisions;
categorized.permanent.securityRequirements = contextData.project.securityRequirements;
categorized.permanent.complianceRequirements = contextData.project.complianceRequirements;
}
// Critical content (light compression)
if (contextData.session) {
categorized.critical.currentSprint = contextData.session.currentSprint;
categorized.critical.activeWork = contextData.session.activeWork;
categorized.critical.recentHandoffs = this.getRecentItems(contextData.session.handoffs, 5);
categorized.critical.blockers = contextData.session.blockers;
}
if (contextData.agents) {
categorized.critical.activeAgents = contextData.agents.activeAgents;
categorized.critical.pendingHandoffs = contextData.agents.handoffQueue;
}
// Important content (moderate compression)
if (contextData.session) {
categorized.important.recentDecisions = this.getRecentItems(contextData.session.decisions, 10);
categorized.important.completedFeatures = this.getRecentItems(contextData.session.completedFeatures, 15);
categorized.important.testResults = this.getRecentItems(contextData.session.testResults, 5);
}
// Historical content (aggressive compression)
if (contextData.session) {
categorized.historical.oldHandoffs = this.getOlderItems(contextData.session.handoffs, 5);
categorized.historical.oldDecisions = this.getOlderItems(contextData.session.decisions, 10);
categorized.historical.archivedFeatures = this.getOlderItems(contextData.session.completedFeatures, 15);
}
// Temporary content (most aggressive compression)
if (contextData.debug) {
categorized.temporary.debugLogs = contextData.debug.logs;
categorized.temporary.explorationWork = contextData.debug.exploration;
categorized.temporary.experimentalCode = contextData.debug.experimental;
}
return categorized;
}
/**
* Compress critical content (preserve most detail)
*/
async compressCritical(content, aggressive = false) {
if (!content || Object.keys(content).length === 0) return content;
const compressed = {};
// Keep current sprint full detail
if (content.currentSprint) {
compressed.currentSprint = content.currentSprint;
}
// Compress recent handoffs but keep key information
if (content.recentHandoffs) {
compressed.recentHandoffs = content.recentHandoffs.map(handoff => ({
from: handoff.from,
to: handoff.to,
task: this.summarizeText(handoff.task, aggressive ? 50 : 100),
status: handoff.status,
timestamp: handoff.timestamp,
keyOutcomes: handoff.keyOutcomes || handoff.result?.summary
}));
}
// Keep active work with light compression
if (content.activeWork) {
compressed.activeWork = this.compressActiveWork(content.activeWork, aggressive);
}
// Keep blockers - these are critical
if (content.blockers) {
compressed.blockers = content.blockers;
}
return compressed;
}
/**
* Compress important content (moderate compression)
*/
async compressImportant(content, aggressive = false) {
if (!content || Object.keys(content).length === 0) return content;
const compressed = {};
// Compress recent decisions to key points
if (content.recentDecisions) {
compressed.recentDecisions = content.recentDecisions.map(decision => ({
title: decision.title,
decision: this.summarizeText(decision.decision, aggressive ? 30 : 60),
rationale: this.summarizeText(decision.rationale, aggressive ? 20 : 40),
date: decision.date,
impact: decision.impact
}));
}
// Compress completed features to outcomes
if (content.completedFeatures) {
compressed.completedFeatures = content.completedFeatures.map(feature => ({
name: feature.name,
summary: this.summarizeText(feature.description, aggressive ? 25 : 50),
completedDate: feature.completedDate,
keyComponents: feature.keyComponents?.slice(0, 3), // Top 3 components
testsPassing: feature.testsPassing
}));
}
// Compress test results to key metrics
if (content.testResults) {
compressed.testResults = content.testResults.map(result => ({
type: result.type,
status: result.status,
coverage: result.coverage,
criticalFailures: result.criticalFailures,
date: result.date
}));
}
return compressed;
}
/**
* Compress historical content (aggressive compression)
*/
async compressHistorical(content) {
if (!content || Object.keys(content).length === 0) return {};
const compressed = {};
// Compress old handoffs to just outcomes
if (content.oldHandoffs && content.oldHandoffs.length > 0) {
compressed.historicalHandoffs = {
count: content.oldHandoffs.length,
timeRange: {
earliest: content.oldHandoffs[0]?.timestamp,
latest: content.oldHandoffs[content.oldHandoffs.length - 1]?.timestamp
},
keyOutcomes: content.oldHandoffs
.filter(h => h.keyOutcomes)
.map(h => h.keyOutcomes)
.slice(0, 5) // Top 5 historical outcomes
};
}
// Compress old decisions to decision log
if (content.oldDecisions && content.oldDecisions.length > 0) {
compressed.decisionLog = content.oldDecisions.map(decision => ({
title: decision.title,
date: decision.date,
outcome: this.summarizeText(decision.decision, 20)
}));
}
// Compress archived features to feature list
if (content.archivedFeatures && content.archivedFeatures.length > 0) {
compressed.featureHistory = {
totalFeatures: content.archivedFeatures.length,
majorFeatures: content.archivedFeatures
.filter(f => f.importance === 'major')
.map(f => ({
name: f.name,
completedDate: f.completedDate
}))
};
}
return compressed;
}
/**
* Generate high-level context summary
*/
async generateContextSummary(contextData) {
const summary = {
projectState: this.summarizeProjectState(contextData),
development: this.summarizeDevelopmentProgress(contextData),
teamStatus: this.summarizeTeamStatus(contextData),
upcomingWork: this.summarizeUpcomingWork(contextData)
};
return summary;
}
/**
* Compress active work details
*/
compressActiveWork(activeWork, aggressive = false) {
const compressed = {};
for (const [workId, work] of Object.entries(activeWork)) {
compressed[workId] = {
title: work.title,
status: work.status,
assignedAgent: work.assignedAgent,
progress: work.progress,
keyTasks: work.tasks?.slice(0, aggressive ? 2 : 5), // Limit tasks
blockedOn: work.blockedOn,
estimatedCompletion: work.estimatedCompletion
};
// Remove detailed descriptions if aggressive
if (!aggressive && work.description) {
compressed[workId].description = this.summarizeText(work.description, 75);
}
}
return compressed;
}
/**
* Smart text summarization
*/
summarizeText(text, maxWords = 50) {
if (!text || typeof text !== 'string') return text;
const words = text.trim().split(/\s+/);
if (words.length <= maxWords) return text;
// Keep first part and important keywords
const summary = words.slice(0, maxWords).join(' ');
return summary + '...';
}
/**
* Get recent items from array
*/
getRecentItems(items, count) {
if (!Array.isArray(items)) return [];
return items.slice(-count);
}
/**
* Get older items from array
*/
getOlderItems(items, skipRecent) {
if (!Array.isArray(items)) return [];
return items.slice(0, -skipRecent);
}
/**
* Estimate token count (rough approximation)
*/
estimateTokenCount(text) {
if (!text) return 0;
// Rough estimate: ~4 characters per token
return Math.ceil(text.length / 4);
}
/**
* Generate compression report
*/
generateCompressionReport(original, compressed) {
const report = {
compressionTargets: {},
tokensReduced: 0,
itemsPreserved: 0,
itemsCompressed: 0
};
for (const category of ['permanent', 'critical', 'important', 'historical', 'temporary']) {
const originalSize = this.estimateTokenCount(JSON.stringify(original[category] || {}));
const compressedSize = this.estimateTokenCount(JSON.stringify(compressed[category] || {}));
report.compressionTargets[category] = {
originalTokens: originalSize,
compressedTokens: compressedSize,
reduction: originalSize > 0 ? (1 - compressedSize / originalSize) * 100 : 0
};
report.tokensReduced += originalSize - compressedSize;
}
return report;
}
// Summary helper methods
summarizeProjectState(contextData) {
return {
phase: contextData.project?.currentPhase || 'development',
architecture: contextData.project?.architecture?.type || 'unknown',
lastMajorChange: contextData.project?.lastMajorChange,
healthScore: this.calculateProjectHealth(contextData)
};
}
summarizeDevelopmentProgress(contextData) {
const completed = contextData.session?.completedFeatures?.length || 0;
const inProgress = Object.keys(contextData.session?.activeWork || {}).length;
return {
featuresCompleted: completed,
activeWork: inProgress,
recentVelocity: this.calculateVelocity(contextData.session?.completedFeatures),
blockers: contextData.session?.blockers?.length || 0
};
}
summarizeTeamStatus(contextData) {
const agents = contextData.agents || {};
const active = Object.keys(agents.activeAgents || {}).length;
const pending = agents.handoffQueue?.length || 0;
return {
activeAgents: active,
pendingHandoffs: pending,
teamEfficiency: this.calculateTeamEfficiency(agents)
};
}
summarizeUpcomingWork(contextData) {
return {
nextSprint: contextData.session?.nextSprint || 'undefined',
priorityTasks: contextData.session?.priorityTasks?.slice(0, 3) || [],
pendingDecisions: contextData.session?.pendingDecisions?.length || 0
};
}
calculateProjectHealth(contextData) {
// Simple health calculation based on multiple factors
let score = 100;
const blockers = contextData.session?.blockers?.length || 0;
const failingTests = contextData.session?.testResults?.filter(t => t.status === 'failed')?.length || 0;
score -= blockers * 10;
score -= failingTests * 5;
return Math.max(0, Math.min(100, score));
}
calculateVelocity(completedFeatures) {
if (!completedFeatures || completedFeatures.length < 2) return 0;
// Calculate features completed per week over last month
const now = new Date();
const lastMonth = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const recentFeatures = completedFeatures.filter(f =>
new Date(f.completedDate) > lastMonth
);
return Math.round((recentFeatures.length / 4) * 10) / 10; // Per week
}
calculateTeamEfficiency(agents) {
const active = Object.keys(agents.activeAgents || {}).length;
const total = 20; // Approximate total agents available
return Math.round((active / total) * 100);
}
}
module.exports = ContextCompressor;