codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
879 lines (727 loc) • 24.4 kB
text/typescript
import { logger } from '../logger.js';
import { readFile, writeFile, access, mkdir } from 'fs/promises';
import { join, dirname } from 'path';
import { existsSync } from 'fs';
import { Task } from '../planning/enhanced-agentic-planner.js';
import { registerShutdownHandler, createManagedInterval, clearManagedInterval } from '../agent.js';
export interface ContextItem {
key: string;
value: any;
timestamp: number;
size: number;
relevanceScore: number;
sessionId: string;
type: 'file' | 'conversation' | 'execution' | 'project' | 'temporary';
metadata: {
tags: string[];
source: string;
expiresAt?: number;
priority: 'low' | 'medium' | 'high';
};
}
export interface ContextQuery {
keywords?: string[];
type?: ContextItem['type'];
sessionId?: string;
tags?: string[];
timeRange?: {
start: number;
end: number;
};
maxAge?: number;
minRelevanceScore?: number;
}
export interface ContextSummary {
totalItems: number;
totalSize: number;
typeBreakdown: Record<string, number>;
oldestItem: number;
newestItem: number;
avgRelevanceScore: number;
}
/**
* Enhanced Context Manager
*
* Provides sophisticated context management with:
* - Long-term memory persistence
* - Intelligent context pruning
* - Relevance-based retrieval
* - Cross-session context retention
* - Smart context compression
*/
export class EnhancedContextManager {
private contextStore = new Map<string, ContextItem>();
private maxContextSize: number;
private persistencePath: string;
private currentSessionId: string;
private lastPersistTime: number = 0;
private persistenceInterval: number = 5 * 60 * 1000; // 5 minutes
private persistenceTimer: NodeJS.Timeout | null = null;
constructor(
maxContextSize: number = 50 * 1024 * 1024, // 50MB
persistencePath: string = join(process.cwd(), '.codecrucible', 'context')
) {
this.maxContextSize = maxContextSize;
this.persistencePath = persistencePath;
this.currentSessionId = this.generateSessionId();
this.initializePersistence();
this.startPeriodicPersistence();
// Register for shutdown
registerShutdownHandler(() => this.cleanup());
}
/**
* Add context with automatic relevance scoring and metadata
*/
async addContext(
key: string,
value: any,
options: {
type?: ContextItem['type'];
tags?: string[];
source?: string;
priority?: 'low' | 'medium' | 'high';
expiresIn?: number; // milliseconds
relevanceBoost?: number;
} = {}
): Promise<void> {
const now = Date.now();
const serializedValue = this.serializeValue(value);
const size = this.estimateSize(serializedValue);
// Calculate relevance score
const relevanceScore = this.calculateRelevanceScore(key, value, options);
const contextItem: ContextItem = {
key,
value: serializedValue,
timestamp: now,
size,
relevanceScore,
sessionId: this.currentSessionId,
type: options.type || this.inferType(key, value),
metadata: {
tags: options.tags || [],
source: options.source || 'unknown',
priority: options.priority || 'medium',
expiresAt: options.expiresIn ? now + options.expiresIn : undefined,
},
};
this.contextStore.set(key, contextItem);
// Trigger pruning if needed
await this.pruneContextIfNeeded();
logger.debug(`Context added: ${key} (${size} bytes, relevance: ${relevanceScore.toFixed(2)})`);
}
/**
* Get context with smart relevance-based retrieval
*/
async getContext(key: string): Promise<any> {
const item = this.contextStore.get(key);
if (!item) {
return null;
}
// Check expiration
if (item.metadata.expiresAt && Date.now() > item.metadata.expiresAt) {
this.contextStore.delete(key);
return null;
}
// Update relevance score based on access
item.relevanceScore += 0.1;
item.timestamp = Date.now();
return this.deserializeValue(item.value);
}
/**
* Get context for a specific task with intelligent filtering
*/
async getContextForTask(task: Task): Promise<{
relevantFiles: any[];
conversationHistory: any[];
executionHistory: any[];
projectContext: any[];
suggestions: string[];
}> {
const keywords = this.extractKeywords(task.description);
const query: ContextQuery = {
keywords,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
minRelevanceScore: 0.3,
};
const relevantItems = await this.queryContext(query);
// Categorize results
const categorized = {
relevantFiles: relevantItems.filter(item => item.type === 'file'),
conversationHistory: relevantItems.filter(item => item.type === 'conversation'),
executionHistory: relevantItems.filter(item => item.type === 'execution'),
projectContext: relevantItems.filter(item => item.type === 'project'),
suggestions: this.generateContextSuggestions(task, relevantItems),
};
return categorized;
}
/**
* Query context with sophisticated filtering
*/
async queryContext(query: ContextQuery): Promise<ContextItem[]> {
let results = Array.from(this.contextStore.values());
// Filter by type
if (query.type) {
results = results.filter(item => item.type === query.type);
}
// Filter by session
if (query.sessionId) {
results = results.filter(item => item.sessionId === query.sessionId);
}
// Filter by tags
if (query.tags && query.tags.length > 0) {
results = results.filter(item => query.tags!.some(tag => item.metadata.tags.includes(tag)));
}
// Filter by time range
if (query.timeRange) {
results = results.filter(
item => item.timestamp >= query.timeRange!.start && item.timestamp <= query.timeRange!.end
);
}
// Filter by max age
if (query.maxAge) {
const cutoff = Date.now() - query.maxAge;
results = results.filter(item => item.timestamp >= cutoff);
}
// Filter by relevance score
if (query.minRelevanceScore) {
results = results.filter(item => item.relevanceScore >= query.minRelevanceScore!);
}
// Filter by keywords
if (query.keywords && query.keywords.length > 0) {
results = results.filter(item => this.matchesKeywords(item, query.keywords!));
}
// Sort by relevance score (descending)
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
return results;
}
/**
* Get context summary and statistics
*/
getContextSummary(): ContextSummary {
const items = Array.from(this.contextStore.values());
if (items.length === 0) {
return {
totalItems: 0,
totalSize: 0,
typeBreakdown: {},
oldestItem: 0,
newestItem: 0,
avgRelevanceScore: 0,
};
}
const totalSize = items.reduce((sum, item) => sum + item.size, 0);
const totalRelevance = items.reduce((sum, item) => sum + item.relevanceScore, 0);
const typeBreakdown = items.reduce(
(acc, item) => {
acc[item.type] = (acc[item.type] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
const timestamps = items.map(item => item.timestamp);
return {
totalItems: items.length,
totalSize,
typeBreakdown,
oldestItem: Math.min(...timestamps),
newestItem: Math.max(...timestamps),
avgRelevanceScore: totalRelevance / items.length,
};
}
/**
* Intelligent context pruning with multiple strategies
*/
async pruneContextIfNeeded(): Promise<void> {
const totalSize = this.getTotalSize();
if (totalSize <= this.maxContextSize) {
return;
}
logger.info(`Context size ${totalSize} exceeds limit ${this.maxContextSize}, starting pruning`);
const items = Array.from(this.contextStore.values());
const targetSize = this.maxContextSize * 0.8; // Target 80% of max size
// Strategy 1: Remove expired items
await this.removeExpiredItems();
if (this.getTotalSize() <= targetSize) {
return;
}
// Strategy 2: Remove low-relevance temporary items
await this.removeLowRelevanceItems(0.2);
if (this.getTotalSize() <= targetSize) {
return;
}
// Strategy 3: Age-based pruning with relevance weighting
await this.ageBasedPruning(targetSize);
logger.info(`Context pruning complete. New size: ${this.getTotalSize()}`);
}
/**
* Compress context to optimize memory usage
*/
async compressContext(): Promise<void> {
const items = Array.from(this.contextStore.values());
let compressionSavings = 0;
for (const item of items) {
if (item.type === 'conversation' || item.type === 'execution') {
const originalSize = item.size;
const compressed = this.compressLongText(item.value);
if (compressed.length < item.value.length) {
item.value = compressed;
item.size = this.estimateSize(compressed);
compressionSavings += originalSize - item.size;
// Update relevance slightly down due to compression
item.relevanceScore *= 0.95;
}
}
}
if (compressionSavings > 0) {
logger.info(`Context compression saved ${compressionSavings} bytes`);
}
}
/**
* Merge similar context items to reduce redundancy
*/
async mergeSimilarContext(): Promise<void> {
const items = Array.from(this.contextStore.values());
const groups = this.groupSimilarItems(items);
for (const group of groups) {
if (group.length > 1) {
const merged = this.mergeContextItems(group);
// Remove original items
group.forEach(item => this.contextStore.delete(item.key));
// Add merged item
this.contextStore.set(merged.key, merged);
logger.debug(`Merged ${group.length} similar context items into ${merged.key}`);
}
}
}
/**
* Cross-session context retention
*/
async loadPreviousSessionContext(sessionId?: string): Promise<number> {
try {
const contextFile = join(this.persistencePath, `context-${sessionId || 'latest'}.json`);
if (!existsSync(contextFile)) {
return 0;
}
const data = await readFile(contextFile, 'utf8');
const savedContext = JSON.parse(data);
let loadedCount = 0;
for (const item of savedContext.items || []) {
// Only load non-temporary items from previous sessions
if (item.type !== 'temporary') {
// Reduce relevance for old session data
item.relevanceScore *= 0.7;
item.metadata.tags.push('previous-session');
this.contextStore.set(item.key, item);
loadedCount++;
}
}
logger.info(`Loaded ${loadedCount} context items from previous session`);
return loadedCount;
} catch (error) {
logger.warn('Failed to load previous session context:', error);
return 0;
}
}
/**
* Save context to persistent storage
*/
async saveContext(): Promise<void> {
try {
// Ensure directory exists
if (!existsSync(this.persistencePath)) {
await mkdir(this.persistencePath, { recursive: true });
}
const contextFile = join(this.persistencePath, `context-${this.currentSessionId}.json`);
const latestFile = join(this.persistencePath, 'context-latest.json');
const exportData = {
sessionId: this.currentSessionId,
timestamp: Date.now(),
items: Array.from(this.contextStore.values()),
summary: this.getContextSummary(),
};
const jsonData = JSON.stringify(exportData, null, 2);
// Save both session-specific and latest
await writeFile(contextFile, jsonData, 'utf8');
await writeFile(latestFile, jsonData, 'utf8');
this.lastPersistTime = Date.now();
logger.debug(`Context saved to ${contextFile}`);
} catch (error) {
logger.error('Failed to save context:', error);
}
}
/**
* Private helper methods
*/
private calculateRelevanceScore(key: string, value: any, options: any): number {
let score = 0.5; // Base score
// Priority boost
if (options.priority === 'high') score += 0.3;
else if (options.priority === 'medium') score += 0.1;
// Type-based scoring
const typeScores = {
file: 0.8,
project: 0.9,
conversation: 0.6,
execution: 0.7,
temporary: 0.3,
};
const type = options.type || this.inferType(key, value);
score += typeScores[type as keyof typeof typeScores] || 0.5;
// Size penalty for very large items
const size = this.estimateSize(value);
if (size > 1024 * 1024) {
// 1MB
score -= 0.2;
}
// Tag boost
if (options.tags && options.tags.length > 0) {
score += Math.min(options.tags.length * 0.1, 0.3);
}
// Manual relevance boost
if (options.relevanceBoost) {
score += options.relevanceBoost;
}
return Math.max(0, Math.min(1, score));
}
private inferType(key: string, value: any): ContextItem['type'] {
if (key.includes('file') || key.includes('path')) return 'file';
if (key.includes('conversation') || key.includes('chat')) return 'conversation';
if (key.includes('execution') || key.includes('task')) return 'execution';
if (key.includes('project') || key.includes('structure')) return 'project';
return 'temporary';
}
private extractKeywords(text: string): string[] {
// Simple keyword extraction - could be enhanced with NLP
const words = text
.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3)
.filter(word => !this.isStopWord(word));
// Remove duplicates and return top keywords
return [...new Set(words)].slice(0, 10);
}
private isStopWord(word: string): boolean {
const stopWords = new Set([
'the',
'and',
'for',
'are',
'but',
'not',
'you',
'all',
'can',
'had',
'her',
'was',
'one',
'our',
'out',
'day',
'get',
'has',
'him',
'his',
'how',
'its',
'may',
'new',
'now',
'old',
'see',
'two',
'who',
'boy',
'did',
'has',
'let',
'put',
'say',
'she',
'too',
'use',
]);
return stopWords.has(word);
}
private matchesKeywords(item: ContextItem, keywords: string[]): boolean {
const text = JSON.stringify(item.value).toLowerCase();
const itemKeywords = this.extractKeywords(text);
// Check for keyword overlap
const overlap = keywords.filter(keyword =>
itemKeywords.some(
itemKeyword => itemKeyword.includes(keyword) || keyword.includes(itemKeyword)
)
);
return overlap.length > 0;
}
private generateContextSuggestions(task: Task, relevantItems: ContextItem[]): string[] {
const suggestions: string[] = [];
// File-based suggestions
const fileItems = relevantItems.filter(item => item.type === 'file');
if (fileItems.length > 0) {
suggestions.push(`Consider reviewing ${fileItems.length} relevant files from your project`);
}
// Conversation history suggestions
const conversationItems = relevantItems.filter(item => item.type === 'conversation');
if (conversationItems.length > 0) {
suggestions.push(
`Reference ${conversationItems.length} previous conversations on similar topics`
);
}
// Execution history suggestions
const executionItems = relevantItems.filter(item => item.type === 'execution');
if (executionItems.length > 0) {
suggestions.push(`Learn from ${executionItems.length} previous similar task executions`);
}
// Pattern-based suggestions
const commonTags = this.extractCommonTags(relevantItems);
if (commonTags.length > 0) {
suggestions.push(`Common patterns found: ${commonTags.slice(0, 3).join(', ')}`);
}
return suggestions;
}
private extractCommonTags(items: ContextItem[]): string[] {
const tagCounts = new Map<string, number>();
items.forEach(item => {
item.metadata.tags.forEach(tag => {
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
});
});
return Array.from(tagCounts.entries())
.filter(([, count]) => count > 1)
.sort((a, b) => b[1] - a[1])
.map(([tag]) => tag);
}
private serializeValue(value: any): any {
if (typeof value === 'string') return value;
return JSON.stringify(value);
}
private deserializeValue(value: any): any {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return value;
}
}
return value;
}
private estimateSize(value: any): number {
if (typeof value === 'string') {
return value.length * 2; // Assume UTF-16
}
return JSON.stringify(value).length * 2;
}
private getTotalSize(): number {
return Array.from(this.contextStore.values()).reduce((sum, item) => sum + item.size, 0);
}
private async removeExpiredItems(): Promise<void> {
const now = Date.now();
const toRemove: string[] = [];
for (const [key, item] of this.contextStore) {
if (item.metadata.expiresAt && now > item.metadata.expiresAt) {
toRemove.push(key);
}
}
toRemove.forEach(key => this.contextStore.delete(key));
if (toRemove.length > 0) {
logger.debug(`Removed ${toRemove.length} expired context items`);
}
}
private async removeLowRelevanceItems(threshold: number): Promise<void> {
const toRemove: string[] = [];
for (const [key, item] of this.contextStore) {
if (item.type === 'temporary' && item.relevanceScore < threshold) {
toRemove.push(key);
}
}
toRemove.forEach(key => this.contextStore.delete(key));
if (toRemove.length > 0) {
logger.debug(`Removed ${toRemove.length} low-relevance items`);
}
}
private async ageBasedPruning(targetSize: number): Promise<void> {
const items = Array.from(this.contextStore.values());
// Sort by age and relevance score (older and less relevant first)
items.sort((a, b) => {
const ageScore = a.timestamp - b.timestamp; // Older items have lower score
const relevanceScore = (b.relevanceScore - a.relevanceScore) * 100000; // Higher relevance preferred
return ageScore + relevanceScore;
});
let currentSize = this.getTotalSize();
let removedCount = 0;
for (const item of items) {
if (currentSize <= targetSize) break;
// Don't remove high-priority items unless absolutely necessary
if (item.metadata.priority === 'high' && currentSize > targetSize * 1.1) {
continue;
}
this.contextStore.delete(item.key);
currentSize -= item.size;
removedCount++;
}
if (removedCount > 0) {
logger.debug(`Age-based pruning removed ${removedCount} items`);
}
}
private compressLongText(text: string): string {
if (typeof text !== 'string' || text.length < 1000) {
return text;
}
// Simple compression: remove excessive whitespace and summarize if very long
let compressed = text.replace(/\s+/g, ' ').trim();
if (compressed.length > 5000) {
// Keep first and last parts, summarize middle
const start = compressed.substring(0, 2000);
const end = compressed.substring(compressed.length - 2000);
compressed = start + '\n\n[... content compressed ...]\n\n' + end;
}
return compressed;
}
private groupSimilarItems(items: ContextItem[]): ContextItem[][] {
const groups: ContextItem[][] = [];
const used = new Set<ContextItem>();
for (const item of items) {
if (used.has(item)) continue;
const similarItems = [item];
used.add(item);
// Find similar items (simplified similarity check)
for (const otherItem of items) {
if (used.has(otherItem) || item === otherItem) continue;
if (this.areItemsSimilar(item, otherItem)) {
similarItems.push(otherItem);
used.add(otherItem);
}
}
groups.push(similarItems);
}
return groups;
}
private areItemsSimilar(item1: ContextItem, item2: ContextItem): boolean {
// Check if items are similar enough to merge
if (item1.type !== item2.type) return false;
// Check tag overlap
const tagOverlap = item1.metadata.tags.filter(tag => item2.metadata.tags.includes(tag)).length;
return (
tagOverlap > 0 &&
tagOverlap >= Math.min(item1.metadata.tags.length, item2.metadata.tags.length) * 0.5
);
}
private mergeContextItems(items: ContextItem[]): ContextItem {
if (items.length === 1) return items[0];
// Sort by relevance (highest first)
items.sort((a, b) => b.relevanceScore - a.relevanceScore);
const primary = items[0];
const merged: ContextItem = {
key: `merged_${primary.key}_${items.length}`,
value: {
primary: primary.value,
additional: items.slice(1).map(item => ({
key: item.key,
value: item.value,
timestamp: item.timestamp,
})),
},
timestamp: Math.max(...items.map(item => item.timestamp)),
size: items.reduce((sum, item) => sum + item.size, 0) * 0.8, // Assume 20% compression
relevanceScore: items.reduce((sum, item) => sum + item.relevanceScore, 0) / items.length,
sessionId: primary.sessionId,
type: primary.type,
metadata: {
tags: [...new Set(items.flatMap(item => item.metadata.tags))],
source: 'merged',
priority: items.some(item => item.metadata.priority === 'high') ? 'high' : 'medium',
},
};
return merged;
}
private async initializePersistence(): Promise<void> {
try {
if (!existsSync(this.persistencePath)) {
await mkdir(this.persistencePath, { recursive: true });
}
// Try to load previous context
await this.loadPreviousSessionContext();
} catch (error) {
logger.warn('Failed to initialize context persistence:', error);
}
}
private startPeriodicPersistence(): void {
this.persistenceTimer = createManagedInterval(async () => {
const timeSinceLastPersist = Date.now() - this.lastPersistTime;
if (timeSinceLastPersist >= this.persistenceInterval) {
await this.saveContext();
}
}, 60000); // Check every minute
}
/**
* Shutdown context manager and cleanup resources
*/
async shutdown(): Promise<void> {
if (this.persistenceTimer) {
clearManagedInterval(this.persistenceTimer);
this.persistenceTimer = null;
}
// Save final state before shutdown
await this.saveContext();
this.contextStore.clear();
logger.info('✅ EnhancedContextManager shut down successfully');
}
private generateSessionId(): string {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Public utility methods
*/
/**
* Clear all context
*/
clearContext(): void {
this.contextStore.clear();
logger.info('Context cleared');
}
/**
* Get current session ID
*/
getCurrentSessionId(): string {
return this.currentSessionId;
}
/**
* Start new session
*/
startNewSession(): string {
this.currentSessionId = this.generateSessionId();
logger.info(`Started new session: ${this.currentSessionId}`);
return this.currentSessionId;
}
/**
* Export context for debugging or analysis
*/
exportContext(): any {
return {
sessionId: this.currentSessionId,
timestamp: Date.now(),
items: Array.from(this.contextStore.values()),
summary: this.getContextSummary(),
};
}
/**
* Cleanup resources and save state
*/
async cleanup(): Promise<void> {
try {
// Stop the persistence timer
if (this.persistenceTimer) {
clearInterval(this.persistenceTimer);
this.persistenceTimer = null;
}
// Save current state
await this.saveContext();
// Clear the context store
this.contextStore.clear();
} catch (error) {
console.error('Error during context manager cleanup:', error);
}
}
}