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

421 lines (420 loc) 12.5 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import * as fs from "fs/promises"; import * as path from "path"; import { execSync } from "child_process"; import { logger } from "../../../core/monitoring/logger.js"; class StateReconciler { config; ralphDir = ".ralph"; reconciliationLog = []; constructor(config) { this.config = { precedence: config?.precedence || ["git", "files", "memory"], conflictResolution: config?.conflictResolution || "automatic", syncInterval: config?.syncInterval || 5e3, validateConsistency: config?.validateConsistency ?? true }; } /** * Reconcile state from multiple sources */ async reconcile(sources) { logger.info("Reconciling state from sources", { sources: sources.map((s) => ({ type: s.type, confidence: s.confidence })) }); const sortedSources = this.sortByPrecedence(sources); const conflicts = this.detectConflicts(sortedSources); if (conflicts.length > 0) { logger.warn("State conflicts detected", { count: conflicts.length, fields: conflicts.map((c) => c.field) }); const resolutions = await this.resolveConflicts(conflicts); return this.applyResolutions( sortedSources[0].state, resolutions ); } return this.mergeStates(sortedSources); } /** * Detect conflicts between state sources */ detectConflicts(sources) { const conflicts = []; const fields = /* @__PURE__ */ new Set(); sources.forEach((source) => { Object.keys(source.state).forEach((field) => fields.add(field)); }); for (const field of fields) { const values = sources.filter((s) => s.state[field] !== void 0).map((s) => ({ source: s, value: s.state[field] })); if (values.length > 1 && !this.valuesMatch(values.map((v) => v.value))) { conflicts.push({ field, sources: values.map((v) => v.source), severity: this.assessConflictSeverity(field), suggestedResolution: this.suggestResolution( field, values.map((v) => v.source) ) }); } } return conflicts; } /** * Resolve a single conflict */ async resolveConflict(conflict) { const resolution = await this.resolveConflictByStrategy(conflict); this.reconciliationLog.push(resolution); logger.debug("Conflict resolved", { field: conflict.field, resolution: resolution.source, rationale: resolution.rationale }); return resolution; } /** * Validate state consistency */ async validateConsistency(state) { const errors = []; const warnings = []; try { await this.validateFileSystemState(state, errors, warnings); this.validateGitState(state, errors, warnings); this.validateLogicalConsistency(state, errors, warnings); return { testsPass: errors.length === 0, lintClean: true, buildSuccess: true, errors, warnings }; } catch (error) { errors.push(`Validation failed: ${error.message}`); return { testsPass: false, lintClean: false, buildSuccess: false, errors, warnings }; } } /** * Get state from git */ async getGitState() { try { const currentCommit = execSync("git rev-parse HEAD", { encoding: "utf8" }).trim(); const _branch = execSync("git branch --show-current", { encoding: "utf8" }).trim(); const uncommittedChanges = execSync("git status --porcelain", { encoding: "utf8" }); const ralphCommits = execSync( 'git log --oneline --grep="Ralph iteration"', { encoding: "utf8" } ).split("\n").filter(Boolean); const lastRalphCommit = ralphCommits[0]?.split(" ")[0]; const iteration = ralphCommits.length; return { type: "git", state: { currentCommit, startCommit: lastRalphCommit, iteration, status: uncommittedChanges ? "running" : "completed" }, timestamp: Date.now(), confidence: 0.9 }; } catch (error) { logger.error("Failed to get git state", { error: error.message }); return { type: "git", state: {}, timestamp: Date.now(), confidence: 0.1 }; } } /** * Get state from file system */ async getFileState() { try { const statePath = path.join(this.ralphDir, "state.json"); const iterationPath = path.join(this.ralphDir, "iteration.txt"); const feedbackPath = path.join(this.ralphDir, "feedback.txt"); const taskPath = path.join(this.ralphDir, "task.md"); const criteriaPath = path.join(this.ralphDir, "completion-criteria.md"); const [stateData, iteration, feedback, task, criteria] = await Promise.all([ fs.readFile(statePath, "utf8").catch(() => "{}"), fs.readFile(iterationPath, "utf8").catch(() => "0"), fs.readFile(feedbackPath, "utf8").catch(() => ""), fs.readFile(taskPath, "utf8").catch(() => ""), fs.readFile(criteriaPath, "utf8").catch(() => "") ]); const state = JSON.parse(stateData); return { type: "files", state: { ...state, iteration: parseInt(iteration.trim()), feedback: feedback.trim() || void 0, task: task.trim(), criteria: criteria.trim() }, timestamp: Date.now(), confidence: 0.95 }; } catch (error) { logger.error("Failed to get file state", { error: error.message }); return { type: "files", state: {}, timestamp: Date.now(), confidence: 0.1 }; } } /** * Get state from memory (StackMemory) */ async getMemoryState(loopId) { try { return { type: "memory", state: { loopId, lastUpdateTime: Date.now() }, timestamp: Date.now(), confidence: 0.8 }; } catch (error) { logger.error("Failed to get memory state", { error: error.message }); return { type: "memory", state: {}, timestamp: Date.now(), confidence: 0.1 }; } } /** * Sort sources by configured precedence */ sortByPrecedence(sources) { return sources.sort((a, b) => { const aIndex = this.config.precedence.indexOf(a.type); const bIndex = this.config.precedence.indexOf(b.type); if (aIndex === -1 && bIndex === -1) { return b.confidence - a.confidence; } if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); } /** * Check if values match */ valuesMatch(values) { if (values.length === 0) return true; const first = JSON.stringify(values[0]); return values.every((v) => JSON.stringify(v) === first); } /** * Assess conflict severity */ assessConflictSeverity(field) { const highSeverityFields = ["loopId", "task", "criteria", "status"]; const mediumSeverityFields = ["iteration", "currentCommit", "feedback"]; if (highSeverityFields.includes(field)) return "high"; if (mediumSeverityFields.includes(field)) return "medium"; return "low"; } /** * Suggest resolution based on field and sources */ suggestResolution(field, sources) { const sorted = this.sortByPrecedence(sources); const highestConfidence = sorted.reduce( (max, s) => s.confidence > max.confidence ? s : max ); return highestConfidence.state[field]; } /** * Resolve conflicts based on configured strategy */ async resolveConflicts(conflicts) { const resolutions = []; for (const conflict of conflicts) { const resolution = await this.resolveConflictByStrategy(conflict); resolutions.push(resolution); } return resolutions; } /** * Resolve conflict based on strategy */ async resolveConflictByStrategy(conflict) { switch (this.config.conflictResolution) { case "automatic": return this.automaticResolution(conflict); case "manual": return this.manualResolution(conflict); case "interactive": return this.interactiveResolution(conflict); default: return this.automaticResolution(conflict); } } /** * Automatic resolution based on precedence and confidence */ automaticResolution(conflict) { const sorted = this.sortByPrecedence(conflict.sources); const winner = sorted[0]; return { field: conflict.field, value: winner.state[conflict.field], source: winner.type, rationale: `Automatic resolution: ${winner.type} has highest precedence (confidence: ${winner.confidence})` }; } /** * Manual resolution (uses suggested resolution) */ manualResolution(conflict) { return { field: conflict.field, value: conflict.suggestedResolution, source: "manual", rationale: "Manual resolution: using suggested value" }; } /** * Interactive resolution (would prompt user in real implementation) */ async interactiveResolution(conflict) { logger.info("Interactive resolution required", { field: conflict.field, options: conflict.sources.map((s) => ({ type: s.type, value: s.state[conflict.field], confidence: s.confidence })) }); return this.automaticResolution(conflict); } /** * Apply resolutions to base state */ applyResolutions(baseState, resolutions) { const resolvedState = { ...baseState }; for (const resolution of resolutions) { resolvedState[resolution.field] = resolution.value; } return resolvedState; } /** * Merge states without conflicts */ mergeStates(sources) { const merged = {}; for (const source of sources) { Object.assign(merged, source.state); } return merged; } /** * Validate file system state */ async validateFileSystemState(state, errors, warnings) { try { const ralphDirExists = await fs.stat(this.ralphDir).then(() => true).catch(() => false); if (!ralphDirExists) { warnings.push("Ralph directory does not exist"); return; } const requiredFiles = ["task.md", "state.json", "iteration.txt"]; for (const file of requiredFiles) { const filePath = path.join(this.ralphDir, file); const exists = await fs.stat(filePath).then(() => true).catch(() => false); if (!exists) { warnings.push(`Missing file: ${file}`); } } } catch (error) { errors.push(`File system validation failed: ${error.message}`); } } /** * Validate git state */ validateGitState(state, errors, warnings) { try { const isGitRepo = execSync("git rev-parse --is-inside-work-tree", { encoding: "utf8" }).trim() === "true"; if (!isGitRepo) { warnings.push("Not in a git repository"); } if (state.currentCommit && state.startCommit) { try { execSync(`git rev-parse ${state.currentCommit}`, { encoding: "utf8" }); } catch { errors.push(`Invalid current commit: ${state.currentCommit}`); } try { execSync(`git rev-parse ${state.startCommit}`, { encoding: "utf8" }); } catch { warnings.push(`Invalid start commit: ${state.startCommit}`); } } } catch (error) { warnings.push(`Git validation failed: ${error.message}`); } } /** * Validate logical consistency */ validateLogicalConsistency(state, errors, warnings) { if (state.iteration < 0) { errors.push("Invalid iteration number: cannot be negative"); } if (state.status === "completed" && !state.completionData) { warnings.push("Status is completed but no completion data"); } if (state.lastUpdateTime && state.startTime && state.lastUpdateTime < state.startTime) { errors.push("Last update time is before start time"); } if (!state.task) { errors.push("No task defined"); } if (!state.criteria) { warnings.push("No completion criteria defined"); } } } export { StateReconciler };