@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
864 lines (863 loc) • 26.6 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 { 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 { createTransformersProvider } from "../../../core/database/transformers-embedding-provider.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 {
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 {
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);
const embeddingProvider = await createTransformersProvider() ?? void 0;
return new SQLiteAdapter(projectId, { dbPath, embeddingProvider });
}
/**
* 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
};