@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
137 lines (136 loc) • 4.41 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 { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { logger } from "../core/monitoring/logger.js";
const STACKMEMORY_PROCESS_PATTERNS = [
"stackmemory",
"ralph orchestrate",
"ralph swarm",
"ralph loop",
"hooks start"
];
function getStackmemoryProcesses() {
const processes = [];
try {
const psOutput = execSync(
"ps -eo pid,lstart,command 2>/dev/null || ps -eo pid,start,args 2>/dev/null",
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
);
const lines = psOutput.trim().split("\n").slice(1);
for (const line of lines) {
const isStackmemory = STACKMEMORY_PROCESS_PATTERNS.some(
(pattern) => line.toLowerCase().includes(pattern.toLowerCase())
);
if (!isStackmemory) continue;
const match = line.match(
/^\s*(\d+)\s+(\w+\s+\w+\s+\d+\s+[\d:]+\s+\d+)\s+(.+)$/
);
if (match) {
const pid = parseInt(match[1], 10);
const startTimeStr = match[2];
const command = match[3];
if (pid === process.pid) continue;
const startTime = new Date(startTimeStr);
const ageMs = Date.now() - startTime.getTime();
const ageHours = ageMs / (1e3 * 60 * 60);
processes.push({
pid,
command: command.slice(0, 100),
// Truncate long commands
startTime,
ageHours
});
}
}
} catch (error) {
logger.warn("Failed to get process list:", error);
}
return processes;
}
function hasRecentLogActivity(proc, maxAgeHours) {
const logDir = path.join(os.homedir(), ".stackmemory", "logs");
if (!fs.existsSync(logDir)) return false;
try {
const logFiles = fs.readdirSync(logDir).filter((f) => f.endsWith(".log"));
for (const logFile of logFiles) {
const logPath = path.join(logDir, logFile);
const stats = fs.statSync(logPath);
const logAgeHours = (Date.now() - stats.mtimeMs) / (1e3 * 60 * 60);
if (logAgeHours < maxAgeHours) {
try {
const content = fs.readFileSync(logPath, "utf-8").slice(-1e4);
if (content.includes(`pid:${proc.pid}`) || content.includes(`PID ${proc.pid}`)) {
proc.logFile = logPath;
proc.lastLogActivity = stats.mtime;
return true;
}
} catch {
}
}
}
} catch (error) {
logger.warn("Failed to check log activity:", error);
}
return false;
}
function findStaleProcesses(maxAgeHours = 24) {
const allProcesses = getStackmemoryProcesses();
return allProcesses.filter((proc) => {
if (proc.ageHours < maxAgeHours) return false;
if (hasRecentLogActivity(proc, maxAgeHours)) return false;
return true;
});
}
function killStaleProcesses(processes, dryRun = false) {
const result = {
found: processes,
killed: [],
errors: []
};
for (const proc of processes) {
if (dryRun) {
logger.info(`[DRY RUN] Would kill PID ${proc.pid}: ${proc.command}`);
continue;
}
try {
process.kill(proc.pid, "SIGTERM");
result.killed.push(proc.pid);
logger.info(`Killed stale process ${proc.pid}: ${proc.command}`);
} catch (error) {
const err = error;
if (err.code === "ESRCH") {
result.killed.push(proc.pid);
} else {
result.errors.push({
pid: proc.pid,
error: err.message || "Unknown error"
});
logger.warn(`Failed to kill PID ${proc.pid}:`, err.message);
}
}
}
return result;
}
function cleanupStaleProcesses(options = {}) {
const { maxAgeHours = 24, dryRun = false } = options;
logger.info(`Looking for stale processes older than ${maxAgeHours}h...`);
const staleProcesses = findStaleProcesses(maxAgeHours);
if (staleProcesses.length === 0) {
logger.info("No stale processes found");
return { found: [], killed: [], errors: [] };
}
logger.info(`Found ${staleProcesses.length} stale process(es)`);
return killStaleProcesses(staleProcesses, dryRun);
}
export {
cleanupStaleProcesses,
findStaleProcesses,
getStackmemoryProcesses,
killStaleProcesses
};
//# sourceMappingURL=process-cleanup.js.map