@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.
405 lines (404 loc) • 12.4 kB
JavaScript
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
};
//# sourceMappingURL=state-reconciler.js.map