UNPKG

@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
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 };