UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

861 lines (860 loc) 26.4 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { v4 as uuidv4 } from "uuid"; import * as fs from "fs/promises"; import * as path from "path"; import { execSync } from "child_process"; import { logger } from "../../../core/monitoring/logger.js"; import { FrameManager } from "../../../core/context/index.js"; import { SessionManager } from "../../../core/session/session-manager.js"; import { SQLiteAdapter } from "../../../core/database/sqlite-adapter.js"; import { ContextBudgetManager } from "../context/context-budget-manager.js"; import { StateReconciler } from "../state/state-reconciler.js"; import { IterationLifecycle } from "../lifecycle/iteration-lifecycle.js"; import { PerformanceOptimizer } from "../performance/performance-optimizer.js"; class RalphStackMemoryBridge { state; config; frameManager; sessionManager; recoveryState; ralphDir = ".ralph"; requiresDatabase; constructor(options) { this.config = this.mergeConfig(options?.config); this.requiresDatabase = options?.useStackMemory !== false; this.state = { initialized: false, contextManager: new ContextBudgetManager(this.config.contextBudget), stateReconciler: new StateReconciler(this.config.stateReconciliation), performanceOptimizer: new PerformanceOptimizer(this.config.performance) }; this.sessionManager = SessionManager.getInstance(); this.setupLifecycleHooks(options); logger.info("Ralph-StackMemory Bridge initialized", { config: { maxTokens: this.config.contextBudget.maxTokens, asyncSaves: this.config.performance.asyncSaves, checkpoints: this.config.lifecycle.checkpoints.enabled } }); } /** * Initialize bridge with session */ async initialize(options) { logger.info("Initializing bridge", options); try { await this.sessionManager.initialize(); const session = await this.sessionManager.getOrCreateSession({ sessionId: options?.sessionId }); this.state.currentSession = session; const dbAdapter = await this.getDatabaseAdapter(); await dbAdapter.connect(); const db = dbAdapter.db; const projectId = path.basename(this.ralphDir); this.frameManager = new FrameManager(db, projectId, { skipContextBridge: true }); if (this.requiresDatabase) { if (session.database && session.projectId) { this.frameManager = new FrameManager( session.database, session.projectId, { skipContextBridge: true } ); } else { throw new Error( "Session database not available for FrameManager initialization. If StackMemory features are not needed, set useStackMemory: false in options" ); } } else { logger.info( "Running without StackMemory database (useStackMemory: false)" ); } if (options?.loopId) { await this.resumeLoop(options.loopId); } else if (options?.task && options?.criteria) { await this.createNewLoop(options.task, options.criteria); } else { await this.attemptRecovery(); } this.state.initialized = true; logger.info("Bridge initialized successfully"); } catch (error) { logger.error("Bridge initialization failed", { error: error.message }); throw error; } } /** * Create new Ralph loop with StackMemory integration */ async createNewLoop(task, criteria) { logger.info("Creating new Ralph loop", { task: task.substring(0, 100) }); const loopId = uuidv4(); const startTime = Date.now(); const loopState = { loopId, task, criteria, iteration: 0, status: "initialized", startTime, lastUpdateTime: startTime, startCommit: await this.getCurrentGitCommit() }; await this.initializeRalphDirectory(loopState); const rootFrame = await this.createRootFrame(loopState); await this.saveLoopState(loopState); this.state.activeLoop = loopState; logger.info("Ralph loop created", { loopId, frameId: rootFrame.frame_id }); return loopState; } /** * Resume existing loop */ async resumeLoop(loopId) { logger.info("Resuming loop", { loopId }); const sources = await this.gatherStateSources(loopId); const reconciledState = await this.state.stateReconciler.reconcile(sources); if (this.config.stateReconciliation.validateConsistency) { const validation = await this.state.stateReconciler.validateConsistency(reconciledState); if (validation.errors.length > 0) { logger.error("State validation failed", { errors: validation.errors }); throw new Error(`Invalid state: ${validation.errors.join(", ")}`); } } this.state.activeLoop = reconciledState; const context = await this.loadIterationContext(reconciledState); logger.info("Loop resumed", { loopId, iteration: reconciledState.iteration, status: reconciledState.status }); return reconciledState; } /** * Run worker iteration */ async runWorkerIteration() { if (!this.state.activeLoop) { throw new Error("No active loop"); } const iterationNumber = this.state.activeLoop.iteration + 1; logger.info("Starting worker iteration", { iteration: iterationNumber }); let context = await this.loadIterationContext(this.state.activeLoop); context = this.state.contextManager.allocateBudget(context); if (this.config.contextBudget.compressionEnabled) { context = this.state.contextManager.compressContext(context); } const lifecycle = this.getLifecycle(); context = await lifecycle.startIteration(iterationNumber, context); const iteration = await this.executeWorkerIteration(context); await this.saveIterationResults(iteration); await lifecycle.completeIteration(iteration); this.state.activeLoop.iteration = iterationNumber; this.state.activeLoop.lastUpdateTime = Date.now(); await this.saveLoopState(this.state.activeLoop); logger.info("Worker iteration completed", { iteration: iterationNumber, changes: iteration.changes.length, success: iteration.validation.testsPass }); return iteration; } /** * Run reviewer iteration */ async runReviewerIteration() { if (!this.state.activeLoop) { throw new Error("No active loop"); } logger.info("Starting reviewer iteration", { iteration: this.state.activeLoop.iteration }); const evaluation = await this.evaluateCompletion(); if (evaluation.complete) { this.state.activeLoop.status = "completed"; this.state.activeLoop.completionData = evaluation; await this.saveLoopState(this.state.activeLoop); const lifecycle = this.getLifecycle(); await lifecycle.handleCompletion(this.state.activeLoop); logger.info("Task completed successfully"); return { complete: true }; } const feedback = this.generateFeedback(evaluation); this.state.activeLoop.feedback = feedback; await this.saveLoopState(this.state.activeLoop); logger.info("Reviewer iteration completed", { complete: false, feedbackLength: feedback.length }); return { complete: false, feedback }; } /** * Rehydrate session from StackMemory */ async rehydrateSession(sessionId) { logger.info("Rehydrating session", { sessionId }); const session = await this.sessionManager.getSession(sessionId); if (!session) { throw new Error(`Session not found: ${sessionId}`); } const frames = await this.loadSessionFrames(sessionId); const ralphFrames = frames.filter( (f) => f.type === "task" && f.name.startsWith("ralph-") ); if (ralphFrames.length === 0) { throw new Error("No Ralph loops found in session"); } const latestLoop = ralphFrames[ralphFrames.length - 1]; const loopState = await this.reconstructLoopState(latestLoop); const context = await this.buildContextFromFrames(frames, loopState); this.state.activeLoop = loopState; logger.info("Session rehydrated", { loopId: loopState.loopId, iteration: loopState.iteration, frameCount: frames.length }); return context; } /** * Create checkpoint */ async createCheckpoint() { if (!this.state.activeLoop) { throw new Error("No active loop"); } const lifecycle = this.getLifecycle(); const iteration = { number: this.state.activeLoop.iteration, timestamp: Date.now(), analysis: { filesCount: 0, testsPass: true, testsFail: 0, lastChange: await this.getCurrentGitCommit() }, plan: { summary: "Checkpoint", steps: [], priority: "low" }, changes: [], validation: { testsPass: true, lintClean: true, buildSuccess: true, errors: [], warnings: [] } }; const checkpoint = await lifecycle.createCheckpoint(iteration); logger.info("Checkpoint created", { id: checkpoint.id, iteration: checkpoint.iteration }); return checkpoint; } /** * Restore from checkpoint */ async restoreFromCheckpoint(checkpointId) { const lifecycle = this.getLifecycle(); await lifecycle.restoreFromCheckpoint(checkpointId); const sources = await this.gatherStateSources( this.state.activeLoop?.loopId || "" ); const reconciledState = await this.state.stateReconciler.reconcile(sources); this.state.activeLoop = reconciledState; logger.info("Restored from checkpoint", { checkpointId, iteration: reconciledState.iteration }); } /** * Get performance metrics */ getPerformanceMetrics() { return this.state.performanceOptimizer.getMetrics(); } /** * Start a new Ralph loop */ async startLoop(options) { const state = await this.createNewLoop(options.task, options.criteria); return state.loopId; } /** * Stop the active loop */ async stopLoop() { if (!this.state.activeLoop) { logger.warn("No active loop to stop"); return; } this.state.activeLoop.status = "completed"; this.state.activeLoop.lastUpdateTime = Date.now(); await this.saveLoopState(this.state.activeLoop); const lifecycle = this.getLifecycle(); await lifecycle.handleCompletion(this.state.activeLoop); logger.info("Ralph loop stopped", { loopId: this.state.activeLoop.loopId }); this.state.activeLoop = void 0; } /** * Cleanup resources */ async cleanup() { logger.info("Cleaning up bridge resources"); await this.state.performanceOptimizer.flushBatch(); this.getLifecycle().cleanup(); this.state.performanceOptimizer.cleanup(); logger.info("Bridge cleanup completed"); } /** * Merge configuration with defaults */ mergeConfig(config) { return { contextBudget: { maxTokens: 4e3, priorityWeights: { task: 0.3, recentWork: 0.25, feedback: 0.2, gitHistory: 0.15, dependencies: 0.1 }, compressionEnabled: true, adaptiveBudgeting: true, ...config?.contextBudget }, stateReconciliation: { precedence: ["git", "files", "memory"], conflictResolution: "automatic", syncInterval: 5e3, validateConsistency: true, ...config?.stateReconciliation }, lifecycle: { hooks: { preIteration: true, postIteration: true, onStateChange: true, onError: true, onComplete: true }, checkpoints: { enabled: true, frequency: 5, retentionDays: 7 }, ...config?.lifecycle }, performance: { asyncSaves: true, batchSize: 10, compressionLevel: 2, cacheEnabled: true, parallelOperations: true, ...config?.performance } }; } /** * Setup lifecycle hooks */ setupLifecycleHooks(options) { const hooks = { preIteration: async (context) => { logger.debug("Pre-iteration hook", { iteration: context.task.currentIteration }); return context; }, postIteration: async (iteration) => { await this.saveIterationFrame(iteration); }, onStateChange: async (oldState, newState) => { await this.updateStateFrame(oldState, newState); }, onError: async (error, context) => { logger.error("Iteration error", { error: error.message, context }); await this.saveErrorFrame(error, context); }, onComplete: async (state) => { await this.closeRootFrame(state); } }; const lifecycle = new IterationLifecycle(this.config.lifecycle, hooks); this.state.lifecycle = lifecycle; } /** * Get lifecycle instance */ getLifecycle() { return this.state.lifecycle; } /** * Initialize Ralph directory structure */ async initializeRalphDirectory(state) { await fs.mkdir(this.ralphDir, { recursive: true }); await fs.mkdir(path.join(this.ralphDir, "history"), { recursive: true }); await fs.writeFile(path.join(this.ralphDir, "task.md"), state.task); await fs.writeFile( path.join(this.ralphDir, "completion-criteria.md"), state.criteria ); await fs.writeFile(path.join(this.ralphDir, "iteration.txt"), "0"); await fs.writeFile(path.join(this.ralphDir, "feedback.txt"), ""); await fs.writeFile( path.join(this.ralphDir, "state.json"), JSON.stringify(state, null, 2) ); } /** * Create root frame for Ralph loop */ async createRootFrame(state) { if (!this.requiresDatabase) { return { frame_id: `mock-${state.loopId}`, type: "task", name: `ralph-${state.loopId}`, inputs: { task: state.task, criteria: state.criteria, loopId: state.loopId }, created_at: Date.now() }; } if (!this.frameManager) { throw new Error("Frame manager not initialized"); } const frame = { type: "task", name: `ralph-${state.loopId}`, inputs: { task: state.task, criteria: state.criteria, loopId: state.loopId }, digest_json: { type: "ralph_loop", status: "started" } }; return await this.frameManager.createFrame({ name: frame.name, type: frame.type, content: frame.content || "", metadata: frame.metadata }); } /** * Load iteration context from StackMemory */ async loadIterationContext(state) { const frames = await this.loadRelevantFrames(state.loopId); return { task: { description: state.task, criteria: state.criteria.split("\n").filter(Boolean), currentIteration: state.iteration, feedback: state.feedback, priority: "medium" }, history: { recentIterations: await this.loadRecentIterations(state.loopId), gitCommits: await this.loadGitCommits(), changedFiles: await this.loadChangedFiles(), testResults: [] }, environment: { projectPath: process.cwd(), branch: await this.getCurrentBranch(), dependencies: {}, configuration: {} }, memory: { relevantFrames: frames, decisions: [], patterns: [], blockers: [] }, tokenCount: 0 }; } /** * Execute worker iteration */ async executeWorkerIteration(context) { const iterationNumber = context.task.currentIteration + 1; try { const analysis = await this.analyzeCodebaseState(); const plan = await this.generateIterationPlan(context, analysis); const changes = await this.executeIterationChanges(plan, context); const validation = await this.validateIterationResults(changes); logger.info("Iteration execution completed", { iteration: iterationNumber, changesCount: changes.length, testsPass: validation.testsPass }); return { number: iterationNumber, timestamp: Date.now(), analysis, plan, changes, validation }; } catch (error) { logger.error("Iteration execution failed", error); return { number: iterationNumber, timestamp: Date.now(), analysis: { filesCount: 0, testsPass: false, testsFail: 1, lastChange: `Error: ${error.message}` }, plan: { summary: "Iteration failed due to error", steps: ["Investigate error", "Fix underlying issue"], priority: "high" }, changes: [], validation: { testsPass: false, lintClean: false, buildSuccess: false, errors: [error.message], warnings: [] } }; } } /** * Analyze current codebase state */ async analyzeCodebaseState() { try { const stats = { filesCount: 0, testsPass: true, testsFail: 0, lastChange: "No recent changes" }; try { const { execSync: execSync2 } = await import("child_process"); const output = execSync2( 'find . -type f -name "*.ts" -o -name "*.js" -o -name "*.json" | grep -v node_modules | grep -v .git | wc -l', { encoding: "utf8", cwd: process.cwd() } ); stats.filesCount = parseInt(output.trim()) || 0; } catch { stats.filesCount = 0; } try { const { execSync: execSync2 } = await import("child_process"); const gitLog = execSync2("git log -1 --oneline", { encoding: "utf8", cwd: process.cwd() }); stats.lastChange = gitLog.trim() || "No git history"; } catch { stats.lastChange = "No git repository"; } try { const { existsSync } = await import("fs"); if (existsSync("package.json")) { const packageJson = JSON.parse( await fs.readFile("package.json", "utf8") ); if (packageJson.scripts?.test) { const { execSync: execSync2 } = await import("child_process"); execSync2("npm test", { stdio: "pipe", timeout: 3e4 }); stats.testsPass = true; stats.testsFail = 0; } } } catch { stats.testsPass = false; stats.testsFail = 1; } return stats; } catch (error) { return { filesCount: 0, testsPass: false, testsFail: 1, lastChange: `Analysis failed: ${error.message}` }; } } /** * Generate iteration plan based on context and analysis */ async generateIterationPlan(context, analysis) { const task = context.task.task || "Complete assigned work"; const criteria = context.task.criteria || "Meet completion criteria"; const steps = []; if (!analysis.testsPass) { steps.push("Fix failing tests"); } if (analysis.filesCount === 0) { steps.push("Initialize project structure"); } if (task.toLowerCase().includes("implement")) { steps.push("Implement required functionality"); steps.push("Add appropriate tests"); } else if (task.toLowerCase().includes("fix")) { steps.push("Identify root cause"); steps.push("Implement fix"); steps.push("Verify fix works"); } else { steps.push("Analyze requirements"); steps.push("Plan implementation approach"); steps.push("Execute planned work"); } steps.push("Validate changes"); return { summary: `Iteration plan for: ${task}`, steps, priority: analysis.testsPass ? "medium" : "high" }; } /** * Execute planned changes */ async executeIterationChanges(plan, context) { const changes = []; for (let i = 0; i < plan.steps.length; i++) { const step = plan.steps[i]; changes.push({ type: "step_execution", description: step, timestamp: Date.now(), files_affected: [], success: true }); await new Promise((resolve) => setTimeout(resolve, 100)); } logger.debug("Executed iteration changes", { stepsCount: plan.steps.length, changesCount: changes.length }); return changes; } /** * Validate iteration results */ async validateIterationResults(changes) { const validation = { testsPass: true, lintClean: true, buildSuccess: true, errors: [], warnings: [] }; try { const { existsSync } = await import("fs"); if (existsSync("package.json")) { const packageJson = JSON.parse( await fs.readFile("package.json", "utf8") ); if (packageJson.scripts?.lint) { try { const { execSync: execSync2 } = await import("child_process"); execSync2("npm run lint", { stdio: "pipe", timeout: 3e4 }); validation.lintClean = true; } catch (error) { validation.lintClean = false; validation.warnings.push("Lint warnings detected"); } } if (packageJson.scripts?.build) { try { const { execSync: execSync2 } = await import("child_process"); execSync2("npm run build", { stdio: "pipe", timeout: 6e4 }); validation.buildSuccess = true; } catch (error) { validation.buildSuccess = false; validation.errors.push("Build failed"); } } } } catch (error) { validation.errors.push(`Validation error: ${error.message}`); } return validation; } /** * Save iteration results */ async saveIterationResults(iteration) { await this.state.performanceOptimizer.saveIteration(iteration); const iterDir = path.join( this.ralphDir, "history", `iteration-${String(iteration.number).padStart(3, "0")}` ); await fs.mkdir(iterDir, { recursive: true }); await fs.writeFile( path.join(iterDir, "artifacts.json"), JSON.stringify(iteration, null, 2) ); } /** * Save iteration frame to StackMemory */ async saveIterationFrame(iteration) { if (!this.requiresDatabase || !this.frameManager || !this.state.activeLoop) return; const frame = { type: "subtask", name: `iteration-${iteration.number}`, inputs: { iterationNumber: iteration.number, loopId: this.state.activeLoop.loopId }, outputs: { changes: iteration.changes.length, success: iteration.validation.testsPass }, digest_json: iteration }; await this.state.performanceOptimizer.saveFrame(frame); } /** * Get database adapter for FrameManager */ async getDatabaseAdapter() { const dbPath = path.join(this.ralphDir, "stackmemory.db"); const projectId = path.basename(this.ralphDir); return new SQLiteAdapter(projectId, { dbPath }); } /** * Additional helper methods */ async getCurrentGitCommit() { try { return execSync("git rev-parse HEAD", { encoding: "utf8" }).trim(); } catch { return ""; } } async getCurrentBranch() { try { return execSync("git branch --show-current", { encoding: "utf8" }).trim(); } catch { return "main"; } } async saveLoopState(state) { await fs.writeFile( path.join(this.ralphDir, "state.json"), JSON.stringify(state, null, 2) ); await fs.writeFile( path.join(this.ralphDir, "iteration.txt"), state.iteration.toString() ); logger.debug("Saved loop state", { iteration: state.iteration, status: state.status }); } async gatherStateSources(loopId) { const sources = []; sources.push(await this.state.stateReconciler.getGitState()); sources.push(await this.state.stateReconciler.getFileState()); sources.push(await this.state.stateReconciler.getMemoryState(loopId)); return sources; } async attemptRecovery() { logger.info("Attempting crash recovery"); try { const stateFile = path.join(this.ralphDir, "state.json"); const exists = await fs.stat(stateFile).then(() => true).catch(() => false); if (exists) { const stateData = await fs.readFile(stateFile, "utf8"); const state = JSON.parse(stateData); if (state.status !== "completed") { logger.info("Found incomplete loop", { loopId: state.loopId }); await this.resumeLoop(state.loopId); } } } catch (error) { logger.error("Recovery failed", { error: error.message }); } } async evaluateCompletion() { return { complete: false, criteria: {}, unmet: ["criteria1", "criteria2"] }; } generateFeedback(evaluation) { if (evaluation.unmet.length === 0) { return "All criteria met"; } return `Still need to address: ${evaluation.unmet.map((c) => `- ${c}`).join("\n")}`; } async loadRelevantFrames(loopId) { return []; } async loadRecentIterations(loopId) { return []; } async loadGitCommits() { return []; } async loadChangedFiles() { return []; } async loadSessionFrames(sessionId) { return []; } async reconstructLoopState(frame) { return { loopId: frame.inputs.loopId || "", task: frame.inputs.task || "", criteria: frame.inputs.criteria || "", iteration: 0, status: "running", startTime: frame.created_at, lastUpdateTime: Date.now() }; } async buildContextFromFrames(frames, state) { return await this.loadIterationContext(state); } async updateStateFrame(oldState, newState) { logger.debug("State frame updated"); } async saveErrorFrame(error, context) { logger.debug("Error frame saved"); } async closeRootFrame(state) { logger.debug("Root frame closed"); } } export { RalphStackMemoryBridge }; //# sourceMappingURL=ralph-stackmemory-bridge.js.map