@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
89 lines (88 loc) • 2.72 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";
const DEFAULT_CONFIG = {
contextPressure: { enabled: true, cooldownSec: 60 },
editRecovery: { enabled: true, cooldownSec: 0 },
retrievalQuality: { enabled: true, cooldownSec: 300 },
traceErrorChain: { enabled: true, cooldownSec: 30 },
harnessRegression: { enabled: true, cooldownSec: 0 },
sessionDrift: { enabled: true, cooldownSec: 120 }
};
class FeedbackLoopEngine extends EventEmitter {
config;
lastFired = /* @__PURE__ */ new Map();
history = [];
maxHistory = 200;
constructor(config = {}) {
super();
this.config = { ...DEFAULT_CONFIG, ...config };
}
/**
* Fire a loop if enabled and not in cooldown.
* Returns the LoopEvent if fired, null if skipped.
*/
fire(loopName, trigger, data, action, outcome = "success") {
const cfg = this.config[loopName];
if (!cfg?.enabled) return null;
const now = Date.now();
const lastTime = this.lastFired.get(loopName) || 0;
if (now - lastTime < cfg.cooldownSec * 1e3) return null;
const event = {
loop: loopName,
trigger,
timestamp: now,
data,
action,
outcome
};
this.lastFired.set(loopName, now);
this.history.push(event);
if (this.history.length > this.maxHistory) {
this.history = this.history.slice(-this.maxHistory);
}
this.emit("loop", event);
this.emit(`loop:${loopName}`, event);
return event;
}
/** Get recent loop events, optionally filtered by loop name. */
getHistory(loopName, limit = 50) {
const filtered = loopName ? this.history.filter((e) => e.loop === loopName) : this.history;
return filtered.slice(-limit);
}
/** Get summary stats per loop. */
getStats() {
const stats = {};
for (const event of this.history) {
if (!stats[event.loop]) {
stats[event.loop] = {
fires: 0,
successes: 0,
errors: 0,
lastFired: null
};
}
stats[event.loop].fires++;
if (event.outcome === "success") stats[event.loop].successes++;
if (event.outcome === "error") stats[event.loop].errors++;
stats[event.loop].lastFired = event.timestamp;
}
return stats;
}
/** Update config at runtime. */
updateConfig(partial) {
this.config = { ...this.config, ...partial };
}
/** Get current config. */
getConfig() {
return { ...this.config };
}
}
const feedbackLoops = new FeedbackLoopEngine();
export {
DEFAULT_CONFIG,
FeedbackLoopEngine,
feedbackLoops
};