@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
190 lines (189 loc) • 6.29 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 { existsSync, mkdirSync, writeFileSync } from "fs";
import { join } from "path";
import { execSync } from "child_process";
import { freemem, homedir, totalmem } from "os";
class DaemonMemoryService {
config;
state;
intervalId;
isRunning = false;
onLog;
constructor(config, onLog) {
this.config = config;
this.onLog = onLog;
this.state = {
lastCheckTime: 0,
lastTriggerTime: 0,
triggerCount: 0,
currentRamPercent: 0,
currentHeapPercent: 0,
errors: []
};
}
start() {
if (this.isRunning || !this.config.enabled) {
return;
}
this.isRunning = true;
const intervalMs = this.config.interval * 60 * 1e3;
this.onLog("INFO", "Memory service started", {
interval: this.config.interval,
ramThreshold: this.config.ramThreshold,
heapThreshold: this.config.heapThreshold,
cooldownMinutes: this.config.cooldownMinutes
});
this.checkMemory();
this.intervalId = setInterval(() => {
this.checkMemory();
}, intervalMs);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = void 0;
}
this.isRunning = false;
this.onLog("INFO", "Memory service stopped");
}
getState() {
return { ...this.state };
}
updateConfig(config) {
const wasRunning = this.isRunning;
if (wasRunning) {
this.stop();
}
this.config = { ...this.config, ...config };
if (wasRunning && this.config.enabled) {
this.start();
}
}
checkMemory() {
if (!this.isRunning) return;
try {
const total = totalmem();
const free = freemem();
const ramPercent = (total - free) / total;
const heap = process.memoryUsage();
const heapPercent = heap.heapUsed / heap.heapTotal;
this.state.currentRamPercent = ramPercent;
this.state.currentHeapPercent = heapPercent;
this.state.lastCheckTime = Date.now();
this.onLog("DEBUG", "Memory check", {
ramPercent: Math.round(ramPercent * 100),
heapPercent: Math.round(heapPercent * 100)
});
const ramExceeded = ramPercent > this.config.ramThreshold;
const heapExceeded = heapPercent > this.config.heapThreshold;
if (!ramExceeded && !heapExceeded) return;
const cooldownMs = this.config.cooldownMinutes * 60 * 1e3;
const elapsed = Date.now() - this.state.lastTriggerTime;
if (this.state.lastTriggerTime > 0 && elapsed < cooldownMs) {
this.onLog("DEBUG", "Memory threshold exceeded but in cooldown", {
remainingMs: cooldownMs - elapsed
});
return;
}
const reason = ramExceeded ? `RAM ${Math.round(ramPercent * 100)}% > ${Math.round(this.config.ramThreshold * 100)}%` : `Heap ${Math.round(heapPercent * 100)}% > ${Math.round(this.config.heapThreshold * 100)}%`;
this.onLog("WARN", `Memory threshold exceeded: ${reason}`);
this.triggerCaptureAndClear(reason, ramPercent, heapPercent);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
this.state.errors.push(errorMsg);
this.onLog("ERROR", "Memory check failed", { error: errorMsg });
if (this.state.errors.length > 10) {
this.state.errors = this.state.errors.slice(-10);
}
}
}
triggerCaptureAndClear(reason, ramPercent, heapPercent) {
try {
this.writeSignalFile(reason, ramPercent, heapPercent);
this.state.lastTriggerTime = Date.now();
this.state.triggerCount++;
const stackmemoryBin = this.getStackMemoryBin();
if (stackmemoryBin) {
try {
execSync(`"${stackmemoryBin}" capture --no-commit --basic`, {
timeout: 3e4,
encoding: "utf8",
stdio: "pipe"
});
this.onLog("INFO", "Context captured before memory clear");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.onLog("WARN", "Capture failed", { error: msg });
}
try {
execSync(`"${stackmemoryBin}" clear --save`, {
timeout: 3e4,
encoding: "utf8",
stdio: "pipe"
});
this.onLog("INFO", "Context cleared");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.onLog("WARN", "Clear failed", { error: msg });
}
}
this.onLog("INFO", "Memory trigger completed", {
triggerCount: this.state.triggerCount,
reason
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
this.state.errors.push(errorMsg);
this.onLog("ERROR", "Memory trigger failed", { error: errorMsg });
if (this.state.errors.length > 10) {
this.state.errors = this.state.errors.slice(-10);
}
}
}
writeSignalFile(reason, ramPercent, heapPercent) {
const signalDir = join(process.cwd(), ".stackmemory");
if (!existsSync(signalDir)) {
mkdirSync(signalDir, { recursive: true });
}
const signalPath = join(signalDir, ".memory-clear-signal");
const signal = {
timestamp: Date.now(),
reason,
ramPercent: Math.round(ramPercent * 100),
heapPercent: Math.round(heapPercent * 100)
};
writeFileSync(signalPath, JSON.stringify(signal, null, 2));
this.onLog("INFO", "Signal file written", { path: signalPath });
}
getStackMemoryBin() {
const homeDir = homedir();
const locations = [
join(homeDir, ".stackmemory", "bin", "stackmemory"),
join(homeDir, ".local", "bin", "stackmemory"),
"/usr/local/bin/stackmemory",
"/opt/homebrew/bin/stackmemory"
];
for (const loc of locations) {
if (existsSync(loc)) {
return loc;
}
}
try {
const result = execSync("which stackmemory", {
encoding: "utf8",
stdio: "pipe"
}).trim();
if (result && existsSync(result)) {
return result;
}
} catch {
}
return null;
}
}
export {
DaemonMemoryService
};