@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
449 lines (448 loc) • 12.1 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 { 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();
const timeout = setTimeout(() => {
this.activeTimers.delete(name);
}, 0);
timeout.startTime = start;
this.activeTimers.set(name, timeout);
}
/**
* Stop timer and get duration
*/
stopTimer(name) {
const timer = this.activeTimers.get(name);
if (!timer) return 0;
const duration = Date.now() - (timer.startTime || Date.now());
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
};