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.

449 lines (448 loc) 12.2 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { EventEmitter } from "events"; import { execSync } from "child_process"; import { logger } from "../../../core/monitoring/logger.js"; class IterationLifecycle extends EventEmitter { config; hooks = {}; checkpoints = []; currentIteration; iterationHistory = []; activeTimers = /* @__PURE__ */ new Map(); constructor(config, hooks) { super(); this.config = { hooks: { preIteration: config?.hooks?.preIteration ?? true, postIteration: config?.hooks?.postIteration ?? true, onStateChange: config?.hooks?.onStateChange ?? true, onError: config?.hooks?.onError ?? true, onComplete: config?.hooks?.onComplete ?? true }, checkpoints: { enabled: config?.checkpoints?.enabled ?? true, frequency: config?.checkpoints?.frequency || 5, retentionDays: config?.checkpoints?.retentionDays || 7 } }; if (hooks) { this.registerHooks(hooks); } this.setupEventHandlers(); } /** * Register lifecycle hooks */ registerHooks(hooks) { this.hooks = { ...this.hooks, ...hooks }; logger.debug("Lifecycle hooks registered", { registered: Object.keys(hooks) }); } /** * Start iteration with lifecycle management */ async startIteration(iterationNumber, context) { logger.info("Starting iteration", { iteration: iterationNumber }); this.emitEvent({ type: "iteration.started", timestamp: Date.now(), iteration: iterationNumber, data: { context } }); let processedContext = context; if (this.config.hooks.preIteration && this.hooks.preIteration) { try { processedContext = await this.hooks.preIteration(context); logger.debug("Pre-iteration hook executed", { original: context.tokenCount, processed: processedContext.tokenCount }); } catch (error) { await this.handleError(error, { phase: "preIteration", context }); } } this.startTimer(`iteration-${iterationNumber}`); return processedContext; } /** * Complete iteration with lifecycle management */ async completeIteration(iteration) { logger.info("Completing iteration", { iteration: iteration.number }); this.currentIteration = iteration; const duration = this.stopTimer(`iteration-${iteration.number}`); if (this.config.hooks.postIteration && this.hooks.postIteration) { try { await this.hooks.postIteration(iteration); logger.debug("Post-iteration hook executed"); } catch (error) { await this.handleError(error, { phase: "postIteration", iteration }); } } if (this.shouldCreateCheckpoint(iteration.number)) { await this.createCheckpoint(iteration); } this.emitEvent({ type: "iteration.completed", timestamp: Date.now(), iteration: iteration.number, data: { iteration, duration, success: iteration.validation.testsPass } }); await this.cleanOldCheckpoints(); } /** * Handle iteration failure */ async failIteration(iterationNumber, error, context) { logger.error("Iteration failed", { iteration: iterationNumber, error: error.message }); this.stopTimer(`iteration-${iterationNumber}`); if (this.config.hooks.onError && this.hooks.onError) { try { await this.hooks.onError(error, context); } catch (hookError) { logger.error("Error hook failed", { error: hookError.message }); } } this.emitEvent({ type: "iteration.failed", timestamp: Date.now(), iteration: iterationNumber, data: { error: error.message, stack: error.stack, context } }); } /** * Handle state change */ async handleStateChange(oldState, newState) { logger.debug("State change detected", { old: oldState.status, new: newState.status, iteration: newState.iteration }); if (this.config.hooks.onStateChange && this.hooks.onStateChange) { try { await this.hooks.onStateChange(oldState, newState); } catch (error) { await this.handleError(error, { phase: "stateChange", oldState, newState }); } } this.emitEvent({ type: "state.changed", timestamp: Date.now(), iteration: newState.iteration, data: { oldStatus: oldState.status, newStatus: newState.status, changes: this.detectStateChanges(oldState, newState) } }); if (newState.status === "completed" && oldState.status !== "completed") { await this.handleCompletion(newState); } } /** * Handle loop completion */ async handleCompletion(state) { logger.info("Loop completed", { iterations: state.iteration, duration: state.lastUpdateTime - state.startTime }); if (this.config.hooks.onComplete && this.hooks.onComplete) { try { await this.hooks.onComplete(state); } catch (error) { await this.handleError(error, { phase: "completion", state }); } } await this.createFinalCheckpoint(state); this.cleanupTimers(); } /** * Create checkpoint */ async createCheckpoint(iteration) { const checkpoint = { id: this.generateCheckpointId(), iteration: iteration.number, timestamp: Date.now(), state: await this.captureCurrentState(), gitCommit: await this.getCurrentGitCommit(), verified: false }; checkpoint.verified = await this.verifyCheckpoint(checkpoint); this.checkpoints.push(checkpoint); logger.info("Checkpoint created", { id: checkpoint.id, iteration: checkpoint.iteration, verified: checkpoint.verified }); if (this.hooks.onCheckpoint) { try { await this.hooks.onCheckpoint(checkpoint); } catch (error) { logger.error("Checkpoint hook failed", { error: error.message }); } } this.emitEvent({ type: "checkpoint.created", timestamp: Date.now(), iteration: iteration.number, data: { checkpoint } }); return checkpoint; } /** * Get checkpoints */ getCheckpoints() { return [...this.checkpoints]; } /** * Get last checkpoint */ getLastCheckpoint() { return this.checkpoints[this.checkpoints.length - 1]; } /** * Restore from checkpoint */ async restoreFromCheckpoint(checkpointId) { const checkpoint = this.checkpoints.find((c) => c.id === checkpointId); if (!checkpoint) { throw new Error(`Checkpoint not found: ${checkpointId}`); } logger.info("Restoring from checkpoint", { id: checkpoint.id, iteration: checkpoint.iteration }); if (checkpoint.gitCommit) { await this.restoreGitState(checkpoint.gitCommit); } await this.restoreRalphState(checkpoint.state); logger.info("Checkpoint restored successfully"); } /** * Get iteration events */ getEvents(filter) { let events = [...this.iterationHistory]; if (filter?.type) { events = events.filter((e) => e.type === filter.type); } if (filter?.iteration !== void 0) { events = events.filter((e) => e.iteration === filter.iteration); } if (filter?.since) { events = events.filter((e) => e.timestamp >= filter.since); } return events; } /** * Clean up resources */ cleanup() { this.cleanupTimers(); this.removeAllListeners(); this.iterationHistory = []; this.checkpoints = []; } /** * Setup internal event handlers */ setupEventHandlers() { this.on("*", (event) => { logger.debug("Lifecycle event", { type: event.type, iteration: event.iteration }); }); } /** * Emit and track event */ emitEvent(event) { this.iterationHistory.push(event); this.emit(event.type, event); this.emit("*", event); } /** * Should create checkpoint based on frequency */ shouldCreateCheckpoint(iteration) { if (!this.config.checkpoints.enabled) { return false; } return iteration % this.config.checkpoints.frequency === 0; } /** * Generate checkpoint ID */ generateCheckpointId() { return `chk-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Capture current state */ async captureCurrentState() { return { loopId: "current", task: "", criteria: "", iteration: this.currentIteration?.number || 0, status: "running", startTime: Date.now(), lastUpdateTime: Date.now() }; } /** * Get current git commit */ async getCurrentGitCommit() { try { return execSync("git rev-parse HEAD", { encoding: "utf8" }).trim(); } catch { return ""; } } /** * Verify checkpoint integrity */ async verifyCheckpoint(checkpoint) { try { if (!checkpoint.state.loopId || !checkpoint.state.task) { return false; } if (checkpoint.gitCommit) { execSync(`git rev-parse ${checkpoint.gitCommit}`, { encoding: "utf8" }); } return true; } catch { return false; } } /** * Clean old checkpoints based on retention */ async cleanOldCheckpoints() { const cutoff = Date.now() - this.config.checkpoints.retentionDays * 24 * 60 * 60 * 1e3; const before = this.checkpoints.length; this.checkpoints = this.checkpoints.filter((c) => c.timestamp >= cutoff); const removed = before - this.checkpoints.length; if (removed > 0) { logger.debug("Cleaned old checkpoints", { removed }); } } /** * Create final checkpoint */ async createFinalCheckpoint(state) { const checkpoint = { id: `final-${this.generateCheckpointId()}`, iteration: state.iteration, timestamp: Date.now(), state, gitCommit: await this.getCurrentGitCommit(), verified: true }; this.checkpoints.push(checkpoint); logger.info("Final checkpoint created", { id: checkpoint.id, iterations: state.iteration }); } /** * Restore git state */ async restoreGitState(commit) { execSync("git stash", { encoding: "utf8" }); execSync(`git checkout ${commit}`, { encoding: "utf8" }); } /** * Restore Ralph state */ async restoreRalphState(state) { logger.debug("Ralph state restored", { iteration: state.iteration }); } /** * Detect state changes */ detectStateChanges(oldState, newState) { const changes = []; for (const key of Object.keys(newState)) { if (JSON.stringify(oldState[key]) !== JSON.stringify(newState[key])) { changes.push(key); } } return changes; } /** * Handle errors */ async handleError(error, context) { logger.error("Lifecycle error", { error: error.message, context }); if (this.config.hooks.onError && this.hooks.onError) { try { await this.hooks.onError(error, context); } catch (hookError) { logger.error("Error hook failed", { error: hookError.message }); } } } /** * Start timer for metrics */ startTimer(name) { const start = Date.now(); this.activeTimers.set(name, setTimeout(() => { this.activeTimers.delete(name); }, 0)); this.activeTimers.get(name).startTime = start; } /** * Stop timer and get duration */ stopTimer(name) { const timer = this.activeTimers.get(name); if (!timer) return 0; const duration = Date.now() - timer.startTime; clearTimeout(timer); this.activeTimers.delete(name); return duration; } /** * Clean up all timers */ cleanupTimers() { for (const timer of this.activeTimers.values()) { clearTimeout(timer); } this.activeTimers.clear(); } } export { IterationLifecycle }; //# sourceMappingURL=iteration-lifecycle.js.map