UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

392 lines (391 loc) 12.6 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../../../core/monitoring/logger.js"; import { FrameManager } from "../../../core/context/index.js"; import { sharedContextLayer } from "../../../core/context/shared-context-layer.js"; import { ContextRetriever } from "../../../core/retrieval/context-retriever.js"; import { sessionManager } from "../../../core/session/index.js"; import { ContextBudgetManager } from "./context-budget-manager.js"; class StackMemoryContextLoader { frameManager; contextRetriever; budgetManager; config; constructor(config) { this.config = { maxTokens: 3200, // Leave room for task description lookbackDays: 30, similarityThreshold: 0.7, patternDetectionEnabled: true, includeFailedAttempts: true, crossSessionSearch: true, ...config }; this.budgetManager = new ContextBudgetManager({ maxTokens: this.config.maxTokens, priorityWeights: { task: 0.15, recentWork: 0.3, patterns: 0.25, decisions: 0.2, dependencies: 0.1 } }); logger.info("StackMemory context loader initialized", { maxTokens: this.config.maxTokens, lookbackDays: this.config.lookbackDays, patternDetection: this.config.patternDetectionEnabled }); } async initialize() { try { await sessionManager.initialize(); await sharedContextLayer.initialize(); const session = await sessionManager.getOrCreateSession({}); if (session.database) { this.frameManager = new FrameManager( session.database, session.projectId ); this.contextRetriever = new ContextRetriever(session.database); } logger.info("Context loader initialized successfully"); } catch (error) { logger.error("Failed to initialize context loader", error); throw error; } } /** * Load context for Ralph loop initialization */ async loadInitialContext(request) { logger.info("Loading initial context for Ralph loop", { task: request.task.substring(0, 100), usePatterns: request.usePatterns, useSimilarTasks: request.useSimilarTasks }); const sources = []; let totalTokens = 0; try { if (request.useSimilarTasks) { const similarTasks2 = await this.findSimilarTasks(request.task); if (similarTasks2.length > 0) { const tasksContext = await this.extractTaskContext(similarTasks2); sources.push({ type: "similar_tasks", weight: 0.3, content: tasksContext, tokens: this.budgetManager.estimateTokens(tasksContext) }); totalTokens += sources[sources.length - 1].tokens; } } if (request.usePatterns) { const patterns2 = await this.extractRelevantPatterns(request.task); if (patterns2.length > 0) { const patternsContext = await this.formatPatterns(patterns2); sources.push({ type: "historical_patterns", weight: 0.25, content: patternsContext, tokens: this.budgetManager.estimateTokens(patternsContext) }); totalTokens += sources[sources.length - 1].tokens; } } const decisions = await this.loadRecentDecisions(); if (decisions.length > 0) { const decisionsContext = this.formatDecisions(decisions); sources.push({ type: "recent_decisions", weight: 0.2, content: decisionsContext, tokens: this.budgetManager.estimateTokens(decisionsContext) }); totalTokens += sources[sources.length - 1].tokens; } const projectContext = await this.loadProjectContext(request.task); if (projectContext) { sources.push({ type: "project_context", weight: 0.15, content: projectContext, tokens: this.budgetManager.estimateTokens(projectContext) }); totalTokens += sources[sources.length - 1].tokens; } const budgetedSources = this.budgetManager.allocateBudget({ sources }); const synthesizedContext = this.synthesizeContext( budgetedSources.sources ); logger.info("Context loaded successfully", { totalSources: sources.length, totalTokens, budgetedTokens: budgetedSources.sources.reduce( (sum, s) => sum + s.tokens, 0 ) }); return { context: synthesizedContext, sources: budgetedSources.sources, metadata: { totalTokens: budgetedSources.sources.reduce( (sum, s) => sum + s.tokens, 0 ), sourcesCount: budgetedSources.sources.length, patterns: request.usePatterns ? patterns : [], similarTasks: request.useSimilarTasks ? similarTasks : [] } }; } catch (error) { logger.error("Failed to load context", error); throw error; } } /** * Find similar tasks from StackMemory history */ async findSimilarTasks(taskDescription) { if (!this.frameManager || !this.contextRetriever) { return []; } try { const searchResults = await this.contextRetriever.search( taskDescription, { maxResults: 10, types: ["task", "subtask"], timeFilter: { days: this.config.lookbackDays } } ); const similarities = []; for (const result of searchResults) { const similarity = this.calculateTaskSimilarity( taskDescription, result.content ); if (similarity >= this.config.similarityThreshold) { similarities.push({ frameId: result.frameId, task: result.content, similarity, outcome: await this.determineTaskOutcome(result.frameId), createdAt: result.timestamp, sessionId: result.sessionId || "unknown" }); } } return similarities.sort((a, b) => { const aScore = a.similarity * (a.outcome === "success" ? 1.2 : 1); const bScore = b.similarity * (b.outcome === "success" ? 1.2 : 1); return bScore - aScore; }).slice(0, 5); } catch (error) { logger.error("Failed to find similar tasks", error); return []; } } /** * Extract relevant patterns from historical data */ async extractRelevantPatterns(taskDescription) { try { const context = await sharedContextLayer.getSharedContext(); if (!context) return []; const relevantPatterns = []; for (const pattern of context.globalPatterns) { const relevance = this.calculatePatternRelevance( taskDescription, pattern.pattern ); if (relevance >= 0.5) { relevantPatterns.push({ pattern: pattern.pattern, type: pattern.type, frequency: pattern.frequency, lastSeen: pattern.lastSeen, relevance, resolution: pattern.resolution, examples: await this.getPatternExamples(pattern.pattern) }); } } return relevantPatterns.sort( (a, b) => b.relevance * Math.log(b.frequency + 1) - a.relevance * Math.log(a.frequency + 1) ).slice(0, 8); } catch (error) { logger.error("Failed to extract patterns", error); return []; } } /** * Load recent decisions that might be relevant */ async loadRecentDecisions() { try { const context = await sharedContextLayer.getSharedContext(); if (!context) return []; const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3; return context.decisionLog.filter((d) => d.timestamp >= cutoff && d.outcome === "success").sort((a, b) => b.timestamp - a.timestamp).slice(0, 5); } catch (error) { logger.error("Failed to load recent decisions", error); return []; } } /** * Load project-specific context */ async loadProjectContext(taskDescription) { try { if (!this.contextRetriever) return null; const projectInfo = await this.contextRetriever.search(taskDescription, { maxResults: 3, types: ["task"], projectSpecific: true }); if (projectInfo.length === 0) return null; const contextParts = []; for (const info of projectInfo) { contextParts.push(`Project context: ${info.content}`); } return contextParts.join("\n\n"); } catch (error) { logger.error("Failed to load project context", error); return null; } } /** * Calculate similarity between task descriptions */ calculateTaskSimilarity(task1, task2) { const words1 = new Set(task1.toLowerCase().split(/\s+/)); const words2 = new Set(task2.toLowerCase().split(/\s+/)); const intersection = new Set([...words1].filter((x) => words2.has(x))); const union = /* @__PURE__ */ new Set([...words1, ...words2]); return intersection.size / union.size; } /** * Calculate pattern relevance to current task */ calculatePatternRelevance(taskDescription, pattern) { const taskWords = taskDescription.toLowerCase().split(/\s+/); const patternWords = pattern.toLowerCase().split(/\s+/); let matches = 0; for (const word of taskWords) { if (patternWords.some((p) => p.includes(word) || word.includes(p))) { matches++; } } return matches / taskWords.length; } /** * Extract context from similar tasks */ async extractTaskContext(similarities) { const contextParts = []; contextParts.push("Similar tasks from history:"); for (const sim of similarities) { contextParts.push( ` Task: ${sim.task} Outcome: ${sim.outcome} Similarity: ${Math.round(sim.similarity * 100)}% ${sim.outcome === "success" ? "\u2705 Successfully completed" : "\u274C Had issues"} `.trim() ); } return contextParts.join("\n\n"); } /** * Format patterns for context inclusion */ async formatPatterns(patterns2) { const contextParts = []; contextParts.push("Relevant patterns from experience:"); for (const pattern of patterns2) { contextParts.push( ` Pattern: ${pattern.pattern} Type: ${pattern.type} Frequency: ${pattern.frequency} occurrences ${pattern.resolution ? `Resolution: ${pattern.resolution}` : ""} Relevance: ${Math.round(pattern.relevance * 100)}% `.trim() ); } return contextParts.join("\n\n"); } /** * Format decisions for context inclusion */ formatDecisions(decisions) { const contextParts = []; contextParts.push("Recent successful decisions:"); for (const decision of decisions) { contextParts.push( ` Decision: ${decision.decision} Reasoning: ${decision.reasoning} Date: ${new Date(decision.timestamp).toLocaleDateString()} `.trim() ); } return contextParts.join("\n\n"); } /** * Synthesize all context sources into coherent input */ synthesizeContext(sources) { if (sources.length === 0) { return "No relevant historical context found."; } const contextParts = []; contextParts.push("Context from StackMemory:"); const sortedSources = sources.sort((a, b) => b.weight - a.weight); for (const source of sortedSources) { contextParts.push( ` --- ${source.type.replace("_", " ").toUpperCase()} ---` ); contextParts.push(source.content); } contextParts.push( "\nUse this context to inform your approach to the current task." ); return contextParts.join("\n"); } /** * Determine task outcome from frame history */ async determineTaskOutcome(frameId) { try { if (!this.frameManager) return "unknown"; const frame = await this.frameManager.getFrame(frameId); if (!frame) return "unknown"; if (frame.state === "closed" && frame.outputs) { return "success"; } return frame.state === "closed" ? "failure" : "unknown"; } catch { return "unknown"; } } /** * Get examples of a specific pattern */ async getPatternExamples(_pattern) { return []; } } const stackMemoryContextLoader = new StackMemoryContextLoader(); export { StackMemoryContextLoader, stackMemoryContextLoader };