UNPKG

@claude-vector/claude-tools

Version:

Claude integration tools for AI-powered development assistance

1,749 lines (1,464 loc) 60.5 kB
/** * Context Manager for Claude Vector Search * Manages context window optimization and token calculation * ESM version converted from CommonJS original */ import { EmbeddingGenerator, VectorSearchEngine } from '@claude-vector/core'; /** * Simple LRU Cache implementation */ class LRUCache { constructor(maxSize = 1000) { this.maxSize = maxSize; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return null; // Move to end (most recently used) const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } set(key, value) { // Remove if exists if (this.cache.has(key)) { this.cache.delete(key); } // Add to end this.cache.set(key, value); // Remove oldest if over limit if (this.cache.size > this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } } has(key) { return this.cache.has(key); } clear() { this.cache.clear(); } get size() { return this.cache.size; } } export class ContextManager { constructor(maxTokens = 150000) { this.maxTokens = maxTokens; this.reservedTokens = Math.floor(maxTokens * 0.2); // Reserve 20% for safety this.usedTokens = 0; this.contextItems = []; this.taskDescription = null; this.priorities = { critical: 10, high: 7, medium: 5, low: 3, optional: 1 }; // Long session configuration this.sessionConfig = { maxItems: 5000, // Maximum items to keep in memory targetResponseTime: 50, // Target response time in ms autoCleanupThreshold: 0.9, // Start cleanup when 90% full retentionPolicy: { minAge: 300000, // 5 minutes minimum retention maxAge: 7200000, // 2 hours maximum retention criticalMaxAge: 14400000, // 4 hours for critical items recentAccessBoost: 1800000 // 30 minutes boost for recently accessed }, cleanupBatchSize: 100, // Items to process per cleanup cycle streamingChunkSize: 50 // Items per streaming chunk }; // Smart merge configuration this.mergeConfig = { threshold: 0.85, // 85% similarity for merge embeddingCache: new Map(), // Cache embeddings to avoid re-computation mergeHistory: [], // Track merge operations batchSize: 100 // For batch embedding generation }; // Dynamic priority configuration this.priorityConfig = { accessTracking: new Map(), // Track access frequency per item decayInterval: 3600000, // 1 hour in milliseconds decayFactor: 0.9, // Reduce priority by 10% per hour contextWeights: { development: { code: 1.2, error: 1.5, task: 1.3, general: 1.0 }, debugging: { error: 2.0, code: 1.5, task: 1.1, general: 0.8 }, research: { general: 1.3, code: 1.1, task: 1.0, error: 0.9 }, default: { code: 1.0, error: 1.0, task: 1.0, general: 1.0 } }, learningRate: 0.1, // For user behavior learning feedbackHistory: [] // Store user feedback for learning }; // Initialize embedding generator if API key is available if (process.env.OPENAI_API_KEY) { this.embeddingGenerator = new EmbeddingGenerator(); } // Search integration configuration this.searchConfig = { autoIntegrate: true, relevanceThreshold: 0.7, maxSearchResults: 10, searchContextWindow: 5, // Number of recent searches to consider searchHistory: [], relatedItemsCache: new Map() }; // Initialize search engine reference this.searchEngine = null; // Performance optimization this.performanceConfig = { lruCacheSize: 1000, indexUpdateInterval: 5000, // Update index every 5 seconds enableAsyncProcessing: true }; // Initialize LRU cache this.lruCache = new LRUCache(this.performanceConfig.lruCacheSize); // Initialize in-memory indexes this.indexes = { byType: new Map(), byFile: new Map(), byPriority: new Map(), byTimestamp: new Map() }; // Build initial indexes this.buildInMemoryIndex(); // Start decay timer this.startDecayTimer(); // Start index update timer this.startIndexUpdateTimer(); } /** * Estimate token count for text * Japanese: ~2 chars per token * English: ~4 chars per token */ estimateTokens(text) { if (!text) return 0; const japaneseRegex = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf\u3400-\u4dbf]/g; const japaneseChars = (text.match(japaneseRegex) || []).length; const otherChars = text.length - japaneseChars; // More accurate estimation const japaneseTokens = japaneseChars / 2; const otherTokens = otherChars / 4; return Math.ceil(japaneseTokens + otherTokens); } /** * Add item to context with priority */ addItem(item, priority = 'medium') { // Check if we need cleanup before adding if (this.contextItems.length >= this.sessionConfig.maxItems * this.sessionConfig.autoCleanupThreshold) { this.performAutoCleanup(); } const priorityValue = typeof priority === 'number' ? priority : this.priorities[priority] || this.priorities.medium; const tokens = this.estimateTokens(item.content); const newItem = { id: item.id || this.generateId(), type: item.type || 'general', content: item.content, metadata: item.metadata || {}, tokens, priority: priorityValue, timestamp: Date.now(), source: item.source || 'unknown', lastAccessed: Date.now(), accessCount: 0 }; this.contextItems.push(newItem); // Add to indexes this.addToIndexes(newItem); // Add to cache this.lruCache.set(newItem.id, newItem); this.optimize(); return this.getStats(); } /** * Add multiple items at once */ addItems(items) { for (const item of items) { const priority = item.priority || 'medium'; this.addItem(item, priority); } return this.getStats(); } /** * Remove item by ID */ removeItem(itemId) { const item = this.contextItems.find(i => i.id === itemId); if (item) { this.removeFromIndexes(item); } this.contextItems = this.contextItems.filter(item => item.id !== itemId); this.recalculateTokens(); return this.getStats(); } /** * Clear all items */ clear() { this.contextItems = []; this.usedTokens = 0; return this.getStats(); } /** * Optimize context to fit within token limits */ optimize() { // Calculate priority score (priority per token) const scoredItems = this.contextItems.map(item => ({ ...item, score: this.calculateItemScore(item) })); // Sort by score (highest first) scoredItems.sort((a, b) => b.score - a.score); // Select items that fit within token limit let totalTokens = 0; const optimized = []; const availableTokens = this.maxTokens - this.reservedTokens; for (const item of scoredItems) { if (totalTokens + item.tokens <= availableTokens) { optimized.push(item); totalTokens += item.tokens; } else if (this.canSplitItem(item)) { // Try to fit partial content const remainingTokens = availableTokens - totalTokens; const partialItem = this.splitItem(item, remainingTokens); if (partialItem) { optimized.push(partialItem); totalTokens += partialItem.tokens; } } } this.contextItems = optimized; this.usedTokens = totalTokens; } /** * Calculate item score for optimization */ calculateItemScore(item) { const priorityWeight = 0.5; const recencyWeight = 0.3; const typeWeight = 0.2; // Priority score const priorityScore = item.priority / 10; // Recency score (newer items score higher) const ageInMinutes = (Date.now() - item.timestamp) / (1000 * 60); const recencyScore = Math.max(0, 1 - (ageInMinutes / 60)); // Decay over 1 hour // Type score (some types are more important) const typeScores = { task: 1.0, error: 0.9, code: 0.8, general: 0.7, context: 0.6 }; const typeScore = typeScores[item.type] || 0.5; // Combine scores const combinedScore = ( priorityScore * priorityWeight + recencyScore * recencyWeight + typeScore * typeWeight ) * (item.priority / item.tokens); // Efficiency: priority per token return combinedScore; } /** * Check if item can be split */ canSplitItem(item) { return item.type === 'code' || item.type === 'general' || item.content.length > 200; } /** * Split item to fit remaining tokens */ splitItem(item, remainingTokens) { if (remainingTokens < 50) return null; // Minimum viable content const charPerToken = item.content.match(/[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf\u3400-\u4dbf]/g) ? 2 : 4; const maxChars = Math.floor(remainingTokens * charPerToken * 0.9); // 90% safety margin if (maxChars >= item.content.length) return item; // Find good split point (prefer line breaks) let splitPoint = maxChars; for (let i = maxChars; i > maxChars * 0.7; i--) { if (item.content[i] === '\n') { splitPoint = i; break; } } const truncatedContent = item.content.substring(0, splitPoint) + '\n...'; return { ...item, content: truncatedContent, tokens: this.estimateTokens(truncatedContent), metadata: { ...item.metadata, truncated: true, originalTokens: item.tokens, originalLength: item.content.length } }; } /** * Generate unique ID */ generateId() { return `ctx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Recalculate total tokens */ recalculateTokens() { this.usedTokens = this.contextItems.reduce((total, item) => total + item.tokens, 0); } /** * Get context statistics */ getStats() { const totalItems = this.contextItems.length; const availableTokens = this.maxTokens - this.reservedTokens; const utilizationRate = this.usedTokens / availableTokens; const typeDistribution = {}; const priorityDistribution = {}; for (const item of this.contextItems) { typeDistribution[item.type] = (typeDistribution[item.type] || 0) + 1; priorityDistribution[item.priority] = (priorityDistribution[item.priority] || 0) + 1; } return { totalItems, usedTokens: this.usedTokens, maxTokens: this.maxTokens, availableTokens, reservedTokens: this.reservedTokens, remainingTokens: availableTokens - this.usedTokens, utilizationRate: Math.round(utilizationRate * 100), typeDistribution, priorityDistribution, isOptimized: this.usedTokens <= availableTokens }; } /** * Set task description for context */ setTaskDescription(description) { this.taskDescription = description; if (description) { this.addItem({ type: 'task', content: `Task: ${description}`, source: 'system' }, 'critical'); } } /** * Get formatted context for Claude */ getFormattedContext() { if (this.contextItems.length === 0) { return 'No context available.'; } let formatted = ''; if (this.taskDescription) { formatted += `# Current Task\n${this.taskDescription}\n\n`; } formatted += '# Context Information\n\n'; // Group by type for better organization const groupedItems = {}; for (const item of this.contextItems) { if (!groupedItems[item.type]) { groupedItems[item.type] = []; } groupedItems[item.type].push(item); } // Sort types by importance const typeOrder = ['task', 'error', 'code', 'general', 'context']; for (const type of typeOrder) { if (groupedItems[type]) { formatted += `## ${type.charAt(0).toUpperCase() + type.slice(1)} Information\n\n`; for (const item of groupedItems[type]) { formatted += this.formatItem(item) + '\n\n'; } } } // Add remaining types for (const [type, items] of Object.entries(groupedItems)) { if (!typeOrder.includes(type)) { formatted += `## ${type.charAt(0).toUpperCase() + type.slice(1)} Information\n\n`; for (const item of items) { formatted += this.formatItem(item) + '\n\n'; } } } const stats = this.getStats(); formatted += `---\n*Context Statistics: ${stats.totalItems} items, ${stats.usedTokens}/${stats.availableTokens} tokens (${stats.utilizationRate}% utilization)*`; return formatted; } /** * Format individual context item */ formatItem(item) { let formatted = ''; if (item.metadata && Object.keys(item.metadata).length > 0) { const relevantMetadata = ['file', 'score', 'type', 'language']; for (const key of relevantMetadata) { if (item.metadata[key]) { formatted += `**${key}**: ${item.metadata[key]}\n`; } } } if (formatted) { formatted += '\n'; } formatted += item.content; if (item.metadata && item.metadata.truncated) { formatted += `\n*[Truncated from ${item.metadata.originalTokens} tokens]*`; } return formatted; } /** * Calculate cosine similarity between two vectors */ cosineSimilarity(a, b) { if (!a || !b || a.length !== b.length) return 0; let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } normA = Math.sqrt(normA); normB = Math.sqrt(normB); if (normA === 0 || normB === 0) return 0; return dotProduct / (normA * normB); } /** * Get or generate embedding for content */ async getEmbedding(content) { if (!this.embeddingGenerator) { throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable.'); } // Check cache first const cacheKey = content.substring(0, 100); // Use first 100 chars as key if (this.mergeConfig.embeddingCache.has(cacheKey)) { return this.mergeConfig.embeddingCache.get(cacheKey); } // Generate embedding const embedding = await this.embeddingGenerator.generate(content); // Cache it this.mergeConfig.embeddingCache.set(cacheKey, embedding); // Limit cache size if (this.mergeConfig.embeddingCache.size > 1000) { const firstKey = this.mergeConfig.embeddingCache.keys().next().value; this.mergeConfig.embeddingCache.delete(firstKey); } return embedding; } /** * Detect duplicates for a given item */ async detectDuplicates(item) { const duplicates = []; const nearDuplicates = []; if (!item.content) return { duplicates, nearDuplicates }; // Get embedding for the new item const itemEmbedding = await this.getEmbedding(item.content); // Compare with existing items for (const existingItem of this.contextItems) { // Skip if it's the same item if (existingItem.id === item.id) continue; // Check file path match for same file detection const sameFile = item.metadata?.file && existingItem.metadata?.file && item.metadata.file === existingItem.metadata.file; // Get embedding for existing item const existingEmbedding = await this.getEmbedding(existingItem.content); // Calculate similarity const similarity = this.cosineSimilarity(itemEmbedding, existingEmbedding); if (similarity >= 0.99) { // Nearly identical content duplicates.push({ item: existingItem, similarity, sameFile }); } else if (similarity >= this.mergeConfig.threshold) { // Similar enough to consider merging nearDuplicates.push({ item: existingItem, similarity, sameFile }); } } return { duplicates, nearDuplicates }; } /** * Calculate similarity between two items */ async calculateSimilarity(item1, item2) { if (!item1.content || !item2.content) return 0; const embedding1 = await this.getEmbedding(item1.content); const embedding2 = await this.getEmbedding(item2.content); return this.cosineSimilarity(embedding1, embedding2); } /** * Merge similar items intelligently */ async mergeItems(items) { if (!items || items.length < 2) return items[0]; // Sort by timestamp (newest first) and priority (highest first) items.sort((a, b) => { if (b.timestamp !== a.timestamp) { return b.timestamp - a.timestamp; } return b.priority - a.priority; }); // Use the newest item as base const mergedItem = { ...items[0], metadata: { ...items[0].metadata, mergedFrom: items.map(item => item.id), mergeCount: items.length, originalContents: [] } }; // Keep highest priority mergedItem.priority = Math.max(...items.map(item => item.priority)); // For same file, different versions - keep diffs const sameFile = items.every(item => item.metadata?.file === items[0].metadata?.file ); if (sameFile) { // Store diffs instead of full content for older versions for (let i = 1; i < items.length; i++) { const diff = this.computeDiff(items[i].content, items[0].content); mergedItem.metadata.originalContents.push({ id: items[i].id, timestamp: items[i].timestamp, diff }); } } else { // Different files or no file info - merge content const contents = []; for (const item of items) { const header = item.metadata?.file ? `\n--- ${item.metadata.file} ---\n` : `\n--- Item ${item.id} ---\n`; contents.push(header + item.content); } mergedItem.content = contents.join('\n'); } // Recalculate tokens mergedItem.tokens = this.estimateTokens(mergedItem.content); return mergedItem; } /** * Compute simple diff between two contents */ computeDiff(oldContent, newContent) { // Simple diff: store what was removed and what was added const oldLines = oldContent.split('\n'); const newLines = newContent.split('\n'); const diff = { removed: [], added: [], similarity: 0 }; // Find removed lines oldLines.forEach((line, index) => { if (!newLines.includes(line)) { diff.removed.push({ line: index, content: line }); } }); // Find added lines newLines.forEach((line, index) => { if (!oldLines.includes(line)) { diff.added.push({ line: index, content: line }); } }); // Calculate similarity const commonLines = oldLines.filter(line => newLines.includes(line)).length; diff.similarity = commonLines / Math.max(oldLines.length, newLines.length); return diff; } /** * Track merge history */ trackMergeHistory(merged, originals) { const historyEntry = { timestamp: Date.now(), mergedId: merged.id, originalIds: originals.map(item => item.id), originalTokens: originals.reduce((sum, item) => sum + item.tokens, 0), mergedTokens: merged.tokens, tokensSaved: originals.reduce((sum, item) => sum + item.tokens, 0) - merged.tokens, reason: 'similarity_threshold', similarity: merged.metadata?.averageSimilarity || null }; this.mergeConfig.mergeHistory.push(historyEntry); // Limit history size if (this.mergeConfig.mergeHistory.length > 100) { this.mergeConfig.mergeHistory.shift(); } return historyEntry; } /** * Smart add item with automatic duplicate detection and merging */ async addItemSmart(item, priority = 'medium') { // First detect duplicates const { duplicates, nearDuplicates } = await this.detectDuplicates(item); if (duplicates.length > 0) { // Exact duplicate found - update existing const duplicate = duplicates[0]; const existingIndex = this.contextItems.findIndex( i => i.id === duplicate.item.id ); // Update timestamp and priority this.contextItems[existingIndex].timestamp = Date.now(); this.contextItems[existingIndex].priority = Math.max( this.contextItems[existingIndex].priority, typeof priority === 'number' ? priority : this.priorities[priority] || 5 ); this.optimize(); return { action: 'updated', item: this.contextItems[existingIndex], stats: this.getStats() }; } if (nearDuplicates.length > 0 && nearDuplicates[0].similarity >= this.mergeConfig.threshold) { // Merge with similar items const itemsToMerge = [item, ...nearDuplicates.map(nd => nd.item)]; const mergedItem = await this.mergeItems(itemsToMerge); // Remove original items nearDuplicates.forEach(nd => { this.contextItems = this.contextItems.filter(i => i.id !== nd.item.id); }); // Add merged item this.contextItems.push(mergedItem); // Track merge this.trackMergeHistory(mergedItem, itemsToMerge); this.optimize(); return { action: 'merged', item: mergedItem, mergedCount: itemsToMerge.length, stats: this.getStats() }; } // No duplicates - add normally return { action: 'added', item: this.addItem(item, priority), stats: this.getStats() }; } /** * Track item access for dynamic priority */ trackAccess(itemId) { const access = this.priorityConfig.accessTracking.get(itemId) || { count: 0, lastAccessed: 0, totalScore: 0 }; access.count++; access.lastAccessed = Date.now(); access.totalScore += 1; this.priorityConfig.accessTracking.set(itemId, access); // Update item's dynamic priority const item = this.contextItems.find(i => i.id === itemId); if (item) { this.updateDynamicPriority(item); } } /** * Adjust priority based on task context */ adjustPriorityByContext(item, taskContext = 'default') { const weights = this.priorityConfig.contextWeights[taskContext] || this.priorityConfig.contextWeights.default; const typeWeight = weights[item.type] || 1.0; // Calculate adjusted priority const basePriority = item.priority; const adjustedPriority = basePriority * typeWeight; // Store both base and dynamic priority item.basePriority = basePriority; item.dynamicPriority = adjustedPriority; return adjustedPriority; } /** * Apply time-based decay to priorities */ applyTimeDecay() { const now = Date.now(); for (const item of this.contextItems) { const age = now - item.timestamp; const decayPeriods = Math.floor(age / this.priorityConfig.decayInterval); if (decayPeriods > 0 && !item.noDecay) { // Apply decay factor for each period const decayMultiplier = Math.pow(this.priorityConfig.decayFactor, decayPeriods); // Update dynamic priority with decay const basePriority = item.basePriority || item.priority; item.dynamicPriority = (item.dynamicPriority || basePriority) * decayMultiplier; // Apply access frequency boost to counter decay const access = this.priorityConfig.accessTracking.get(item.id); if (access) { const recencyBoost = this.calculateRecencyBoost(access.lastAccessed); const frequencyBoost = Math.log(access.count + 1) * 0.2; item.dynamicPriority *= (1 + recencyBoost + frequencyBoost); } } } // Re-optimize after decay this.optimize(); } /** * Calculate recency boost based on last access time */ calculateRecencyBoost(lastAccessed) { const timeSinceAccess = Date.now() - lastAccessed; const hoursAgo = timeSinceAccess / 3600000; // Exponential decay for recency boost return Math.exp(-hoursAgo / 24); // Half-life of 24 hours } /** * Learn from user behavior and feedback */ learnFromUserBehavior(feedback) { // Store feedback this.priorityConfig.feedbackHistory.push({ timestamp: Date.now(), itemId: feedback.itemId, action: feedback.action, // 'helpful', 'not_helpful', 'accessed', 'ignored' context: feedback.context, metadata: feedback.metadata }); // Limit history size if (this.priorityConfig.feedbackHistory.length > 1000) { this.priorityConfig.feedbackHistory = this.priorityConfig.feedbackHistory.slice(-500); } // Update item priority based on feedback const item = this.contextItems.find(i => i.id === feedback.itemId); if (item) { const adjustment = this.calculateFeedbackAdjustment(feedback); item.feedbackScore = (item.feedbackScore || 0) + adjustment; // Update dynamic priority this.updateDynamicPriority(item); } // Update context weights if enough feedback if (this.priorityConfig.feedbackHistory.length >= 50) { this.updateContextWeights(); } } /** * Calculate priority adjustment based on feedback */ calculateFeedbackAdjustment(feedback) { const adjustments = { helpful: 0.5, not_helpful: -0.3, accessed: 0.2, ignored: -0.1 }; return adjustments[feedback.action] || 0; } /** * Update context weights based on accumulated feedback */ updateContextWeights() { // Group feedback by context and type const contextStats = {}; for (const feedback of this.priorityConfig.feedbackHistory) { if (!feedback.context) continue; if (!contextStats[feedback.context]) { contextStats[feedback.context] = {}; } const item = this.contextItems.find(i => i.id === feedback.itemId); if (!item) continue; const type = item.type; if (!contextStats[feedback.context][type]) { contextStats[feedback.context][type] = { helpful: 0, total: 0 }; } contextStats[feedback.context][type].total++; if (feedback.action === 'helpful' || feedback.action === 'accessed') { contextStats[feedback.context][type].helpful++; } } // Update weights based on success rate for (const [context, types] of Object.entries(contextStats)) { if (!this.priorityConfig.contextWeights[context]) { this.priorityConfig.contextWeights[context] = { ...this.priorityConfig.contextWeights.default }; } for (const [type, stats] of Object.entries(types)) { const successRate = stats.helpful / stats.total; const currentWeight = this.priorityConfig.contextWeights[context][type] || 1.0; // Gradually adjust weight based on success rate const targetWeight = 0.5 + successRate * 1.5; // Range: 0.5 - 2.0 const newWeight = currentWeight + (targetWeight - currentWeight) * this.priorityConfig.learningRate; this.priorityConfig.contextWeights[context][type] = newWeight; } } } /** * Update dynamic priority for an item */ updateDynamicPriority(item) { const basePriority = item.basePriority || item.priority; let dynamicPriority = basePriority; // Apply access frequency boost const access = this.priorityConfig.accessTracking.get(item.id); if (access) { const recencyBoost = this.calculateRecencyBoost(access.lastAccessed); const frequencyBoost = Math.log(access.count + 1) * 0.2; dynamicPriority *= (1 + recencyBoost + frequencyBoost); } // Apply feedback score if (item.feedbackScore) { dynamicPriority *= (1 + item.feedbackScore * 0.1); } // Apply time decay const age = Date.now() - item.timestamp; const decayPeriods = Math.floor(age / this.priorityConfig.decayInterval); if (decayPeriods > 0 && !item.noDecay) { const decayMultiplier = Math.pow(this.priorityConfig.decayFactor, decayPeriods); dynamicPriority *= decayMultiplier; } item.dynamicPriority = dynamicPriority; } /** * Start the decay timer */ startDecayTimer() { // Apply decay every 10 minutes this.decayTimer = setInterval(() => { this.applyTimeDecay(); }, 600000); // 10 minutes } /** * Stop the decay timer */ stopDecayTimer() { if (this.decayTimer) { clearInterval(this.decayTimer); this.decayTimer = null; } } /** * Get context with dynamic priorities */ getContextWithDynamicPriorities(taskContext = 'default') { // Apply context-based adjustments for (const item of this.contextItems) { this.adjustPriorityByContext(item, taskContext); } // Return formatted context return this.getFormattedContext(); } /** * Override calculateItemScore to use dynamic priority */ calculateItemScore(item) { // Use dynamic priority if available const priority = item.dynamicPriority || item.priority; const priorityWeight = 0.5; const recencyWeight = 0.3; const typeWeight = 0.2; // Priority score const priorityScore = priority / 10; // Recency score (newer items score higher) const ageInMinutes = (Date.now() - item.timestamp) / (1000 * 60); const recencyScore = Math.max(0, 1 - (ageInMinutes / 60)); // Decay over 1 hour // Type score (some types are more important) const typeScores = { task: 1.0, error: 0.9, code: 0.8, general: 0.7, context: 0.6 }; const typeScore = typeScores[item.type] || 0.5; // Combine scores const combinedScore = ( priorityScore * priorityWeight + recencyScore * recencyWeight + typeScore * typeWeight ) * (priority / item.tokens); // Efficiency: priority per token return combinedScore; } /** * Set search engine for integration */ setSearchEngine(searchEngine) { if (searchEngine instanceof VectorSearchEngine) { this.searchEngine = searchEngine; } else { throw new Error('Invalid search engine instance'); } } /** * Integrate search results into context */ async integrateSearchResults(results, query, options = {}) { if (!results || !results.results) return; // Store search in history this.searchConfig.searchHistory.push({ query, timestamp: Date.now(), resultCount: results.results.length, integrated: [] }); // Limit search history if (this.searchConfig.searchHistory.length > this.searchConfig.searchContextWindow) { this.searchConfig.searchHistory.shift(); } const integrated = []; const relevanceThreshold = options.threshold || this.searchConfig.relevanceThreshold; for (const result of results.results) { if (result.score >= relevanceThreshold) { const contextItem = await this.autoContextualize(result, query); // Use smart add to handle duplicates const addResult = await this.addItemSmart(contextItem, this.calculateSearchPriority(result.score)); integrated.push({ itemId: addResult.item.id || contextItem.id, action: addResult.action, score: result.score }); } } // Update search history with integrated items const lastSearch = this.searchConfig.searchHistory[this.searchConfig.searchHistory.length - 1]; lastSearch.integrated = integrated; return { query, integratedCount: integrated.length, totalResults: results.results.length, actions: integrated.map(i => i.action), stats: this.getStats() }; } /** * Auto-contextualize search result */ async autoContextualize(searchResult) { const { chunk, score } = searchResult; // Create context item from search result const contextItem = { type: this.inferItemType(chunk), content: chunk.content, metadata: { file: chunk.file, startLine: chunk.startLine, endLine: chunk.endLine, language: chunk.language || 'unknown', searchScore: score, source: 'search', ...chunk.metadata }, source: 'search' }; // Add surrounding context if available if (this.searchEngine && chunk.file) { const relatedContext = await this.getRelatedContext(chunk); if (relatedContext) { contextItem.content = this.mergeWithContext(chunk.content, relatedContext); contextItem.metadata.hasExtendedContext = true; } } return contextItem; } /** * Get related context for a chunk */ async getRelatedContext(chunk) { const cacheKey = `${chunk.file}:${chunk.startLine}-${chunk.endLine}`; // Check cache if (this.searchConfig.relatedItemsCache.has(cacheKey)) { return this.searchConfig.relatedItemsCache.get(cacheKey); } // Find related chunks in the same file const relatedChunks = []; // This would typically query the search engine for nearby chunks // For now, we'll return null and let the chunk stand alone // In a full implementation, this would fetch surrounding code this.searchConfig.relatedItemsCache.set(cacheKey, relatedChunks); // Limit cache size if (this.searchConfig.relatedItemsCache.size > 100) { const firstKey = this.searchConfig.relatedItemsCache.keys().next().value; this.searchConfig.relatedItemsCache.delete(firstKey); } return relatedChunks.length > 0 ? relatedChunks : null; } /** * Merge chunk content with surrounding context */ mergeWithContext(mainContent, relatedContext) { // Simple merge strategy - could be more sophisticated if (!relatedContext || relatedContext.length === 0) { return mainContent; } // Add context markers let merged = '// === Main Content ===\n'; merged += mainContent; merged += '\n\n// === Related Context ===\n'; merged += relatedContext.map(ctx => ctx.content).join('\n\n'); return merged; } /** * Rank items by relevance to query */ async rankByRelevance(items, query) { if (!this.embeddingGenerator) { // Fallback to simple text matching return this.rankByTextMatch(items, query); } // Get query embedding const queryEmbedding = await this.getEmbedding(query); // Calculate relevance scores const rankedItems = []; for (const item of items) { const itemEmbedding = await this.getEmbedding(item.content); const relevance = this.cosineSimilarity(queryEmbedding, itemEmbedding); rankedItems.push({ item, relevance, originalPriority: item.priority }); } // Sort by relevance rankedItems.sort((a, b) => b.relevance - a.relevance); return rankedItems; } /** * Simple text-based ranking fallback */ rankByTextMatch(items, query) { const queryTerms = query.toLowerCase().split(/\s+/); return items.map(item => { const content = item.content.toLowerCase(); let matchScore = 0; for (const term of queryTerms) { if (content.includes(term)) { matchScore += 1; // Bonus for exact matches const regex = new RegExp(`\\b${term}\\b`, 'g'); const exactMatches = (content.match(regex) || []).length; matchScore += exactMatches * 0.5; } } return { item, relevance: matchScore / queryTerms.length, originalPriority: item.priority }; }).sort((a, b) => b.relevance - a.relevance); } /** * Calculate priority based on search score */ calculateSearchPriority(score) { if (score >= 0.9) return 'high'; if (score >= 0.8) return 'medium'; if (score >= 0.7) return 'low'; return 'optional'; } /** * Infer item type from chunk */ inferItemType(chunk) { const content = chunk.content.toLowerCase(); const file = chunk.file || ''; // Check for error patterns if (content.includes('error') || content.includes('exception') || content.includes('traceback') || content.includes('stack')) { return 'error'; } // Check for task/todo patterns if (content.includes('todo') || content.includes('fixme') || content.includes('task:')) { return 'task'; } // Check file extension const ext = file.split('.').pop(); const codeExtensions = ['js', 'ts', 'py', 'java', 'cpp', 'c', 'go', 'rs', 'rb']; if (codeExtensions.includes(ext)) { return 'code'; } return 'general'; } /** * Get search-aware context */ async getSearchAwareContext(query, taskContext = 'default') { // First, get items ranked by relevance to the query const rankedItems = await this.rankByRelevance(this.contextItems, query); // Apply dynamic priorities with search relevance boost for (const ranked of rankedItems) { const { item, relevance } = ranked; // Boost priority based on relevance const relevanceBoost = relevance * 2; // Up to 2x boost for perfect match const currentPriority = item.dynamicPriority || item.priority; item.searchBoostPriority = currentPriority * (1 + relevanceBoost); } // Temporarily use search-boosted priorities for optimization const originalPriorities = this.contextItems.map(item => ({ id: item.id, priority: item.priority, dynamicPriority: item.dynamicPriority })); // Apply search boost for (const item of this.contextItems) { if (item.searchBoostPriority) { item.priority = item.searchBoostPriority; } } // Optimize with boosted priorities this.optimize(); const context = this.getFormattedContext(); // Restore original priorities for (const original of originalPriorities) { const item = this.contextItems.find(i => i.id === original.id); if (item) { item.priority = original.priority; item.dynamicPriority = original.dynamicPriority; delete item.searchBoostPriority; } } return context; } /** * Search and integrate in one operation */ async searchAndIntegrate(query, options = {}) { if (!this.searchEngine) { throw new Error('Search engine not configured. Use setSearchEngine() first.'); } // Perform search const results = await this.searchEngine.search(query, { threshold: options.threshold || this.searchConfig.relevanceThreshold, maxResults: options.maxResults || this.searchConfig.maxSearchResults, ...options }); // Integrate results const integration = await this.integrateSearchResults(results, query, options); // Get search-aware context const context = await this.getSearchAwareContext(query, options.taskContext); return { searchResults: results, integration, context, stats: this.getStats() }; } /** * Build in-memory indexes for fast access */ buildInMemoryIndex() { // Clear existing indexes for (const index of Object.values(this.indexes)) { index.clear(); } // Build indexes for (const item of this.contextItems) { this.addToIndexes(item); } } /** * Add item to indexes */ addToIndexes(item) { // Index by type if (!this.indexes.byType.has(item.type)) { this.indexes.byType.set(item.type, new Set()); } this.indexes.byType.get(item.type).add(item.id); // Index by file if (item.metadata?.file) { if (!this.indexes.byFile.has(item.metadata.file)) { this.indexes.byFile.set(item.metadata.file, new Set()); } this.indexes.byFile.get(item.metadata.file).add(item.id); } // Index by priority range const priorityBucket = Math.floor(item.priority / 2) * 2; // Bucket by 2s if (!this.indexes.byPriority.has(priorityBucket)) { this.indexes.byPriority.set(priorityBucket, new Set()); } this.indexes.byPriority.get(priorityBucket).add(item.id); // Index by timestamp range (hourly buckets) const hourBucket = Math.floor(item.timestamp / 3600000) * 3600000; if (!this.indexes.byTimestamp.has(hourBucket)) { this.indexes.byTimestamp.set(hourBucket, new Set()); } this.indexes.byTimestamp.get(hourBucket).add(item.id); } /** * Remove item from indexes */ removeFromIndexes(item) { // Remove from type index if (this.indexes.byType.has(item.type)) { this.indexes.byType.get(item.type).delete(item.id); if (this.indexes.byType.get(item.type).size === 0) { this.indexes.byType.delete(item.type); } } // Remove from file index if (item.metadata?.file && this.indexes.byFile.has(item.metadata.file)) { this.indexes.byFile.get(item.metadata.file).delete(item.id); if (this.indexes.byFile.get(item.metadata.file).size === 0) { this.indexes.byFile.delete(item.metadata.file); } } // Remove from priority index const priorityBucket = Math.floor(item.priority / 2) * 2; if (this.indexes.byPriority.has(priorityBucket)) { this.indexes.byPriority.get(priorityBucket).delete(item.id); if (this.indexes.byPriority.get(priorityBucket).size === 0) { this.indexes.byPriority.delete(priorityBucket); } } // Remove from timestamp index const hourBucket = Math.floor(item.timestamp / 3600000) * 3600000; if (this.indexes.byTimestamp.has(hourBucket)) { this.indexes.byTimestamp.get(hourBucket).delete(item.id); if (this.indexes.byTimestamp.get(hourBucket).size === 0) { this.indexes.byTimestamp.delete(hourBucket); } } } /** * Get items by type using index */ getItemsByType(type) { const itemIds = this.indexes.byType.get(type); if (!itemIds) return []; return Array.from(itemIds) .map(id => this.getItemById(id)) .filter(item => item !== null); } /** * Get items by file using index */ getItemsByFile(file) { const itemIds = this.indexes.byFile.get(file); if (!itemIds) return []; return Array.from(itemIds) .map(id => this.getItemById(id)) .filter(item => item !== null); } /** * Get item by ID with LRU cache */ getItemById(id) { // Check cache first const cached = this.lruCache.get(id); if (cached) return cached; // Find in context items const item = this.contextItems.find(i => i.id === id); if (item) { // Cache it this.lruCache.set(id, item); } return item || null; } /** * Start index update timer */ startIndexUpdateTimer() { this.indexUpdateTimer = setInterval(() => { this.buildInMemoryIndex(); }, this.performanceConfig.indexUpdateInterval); } /** * Stop index update timer */ stopIndexUpdateTimer() { if (this.indexUpdateTimer) { clearInterval(this.indexUpdateTimer); this.indexUpdateTimer = null; } } /** * Async process for heavy operations */ async asyncProcess(operation, ...args) { if (!this.performanceConfig.enableAsyncProcessing) { return operation.apply(this, args); } return new Promise((resolve, reject) => { // Use setImmediate for true async processing setImmediate(() => { try { const result = operation.apply(this, args); resolve(result); } catch (error) { reject(error); } }); }); } /** * Monitor memory usage */ monitorMemoryUsage() { const usage = process.memoryUsage(); const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024); const heapTotalMB = Math.round(usage.heapTotal / 1024 / 1024); return { heapUsedMB, heapTotalMB, contextItemsCount: this.contextItems.length, cacheSize: this.lruCache.size, embeddingCacheSize: this.mergeConfig.embeddingCache.size, indexSizes: { byType: this.indexes.byType.size, byFile: this.indexes.byFile.size, byPriority: this.indexes.byPriority.size, byTimestamp: this.indexes.byTimestamp.size } }; } /** * Get performance statistics */ getPerformanceStats() { const memory = this.monitorMemoryUsage(); const stats = this.getStats(); return { ...stats, memory, performance: { cacheHitRate: this.lruCache.size > 0 ? (this.lruCache.size / this.performanceConfig.lruCacheSize) : 0, indexUpdateInterval: this.performanceConfig.indexUpdateInterval, asyncProcessingEnabled: this.performanceConfig.enableAsyncProcessing } }; } /** * Cleanup method to stop all timers */ cleanup() { this.stopDecayTimer(); this.stopIndexUpdateTimer(); this.lruCache.clear(); for (const index of Object.values(this.indexes)) { index.clear(); } } /** * Perform automatic cleanup for long sessions * Intelligently removes items based on relevance, age, and access patterns */ performAutoCleanup() { const startTime = Date.now(); const itemsToRemove = []; const now = Date.now(); // Calculate relevance scores for all items const scoredItems = this.contextItems.map(item => { const score = this.calculateRetentionScore(item, now); return { item, score }; }); // Sort by score (lowest scores will be removed first) scoredItems.sort((a, b) => a.score - b.score); // Determine how many items to remove const targetCount = Math.floor(this.sessionConfig.maxItems * 0.8); // Keep 80% after cleanup const removeCount = Math.max(0, this.contextItems.length - targetCount); // Select items for removal for (let i = 0; i < removeCount && i < scoredItems.length; i++) { const { item, score } = scoredItems[i]; // Never remove critical items less than minAge old if (item.priority >= this.priorities.critical && (now - item.timestamp) < this.sessionConfig.retentionPolicy.minAge) { continue; } itemsToRemove.push(item.id); } // Remove selected items for (const itemId of itemsToRemove) { this.removeItem(itemId); } // Log cleanup performance const duration = Date.now() - startTime; if (this.debugMode) { console.log(`Auto cleanup: removed ${itemsToRemove.length} items in ${duration}ms`); } return { removed: itemsToRemove.length, duration, remaining: this.contextItems.length }; } /** * Calculate retention score for an item * Higher scores mean the item is more likely to be kept */ calculateRetentionScore(item, now) { const age = now - item.timestamp; const timeSinceAccess = now - (item.lastAccessed || item.timestamp); // Base score from priority let score = item.priority || this.priorities.medium; // Boost for recently accessed items if (timeSinceAccess < this.sessionConfig.retentionPolicy.recentAccessBoost) { score *= 2; } // Boost for frequently accessed items if (item.accessCount > 0) { score *= (1 + Math.log(item.accessCount + 1)); } // Penalty for old items const maxAge = item.priority >= this.priorities.critical ? this.sessionConfig.retentionPolicy.criticalMaxAge : this.sessionConfig.retentionPolicy.maxAge; if (age > maxAge) { score *= 0.1; // Severe penalty for very old items } else if (age > maxAge / 2) { score *= 0.5; // Moderate penalty for aging items } // Boost for items with high search relevance if (item.metadata?.searchScore) { score *= (1 + item.metadata.searchScore); } // Boost for items that were helpful (from feedback) if (item.feedbackScore && item.feedbackScore > 0) { score *= (1 + item.feedbackScore); } // Type-based adjustments const typeBoosts = { error: 1.5, // Errors are often referenced task: 1.3, // Tasks define context code: 1.0, // Standard code general: 0.8, // General info less critical context: 0.7 // Context can be regenerated }; score *= typeBoosts[item.type] || 1.0; return score; } /** * Fast search for relevant items (target: <50ms) */ async fastSearch(query, options = {}) { const startTime = Date.now(); const maxResults = options.maxResults || 10; const threshold = options.threshold || 0.7; try { // Use cached embeddings if available const queryKey = query.substring(0, 50); let queryEmbedding; if (this.mergeConfig.embeddingCache.has(queryKey)) { queryEmbedding = this.mergeConfig.embeddingCache.get(queryKey); } else if (this.embed