@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
342 lines (341 loc) • 11.3 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,
writeFileSync,
unlinkSync,
appendFileSync,
readFileSync
} from "fs";
import { isProcessAlive } from "../utils/process-cleanup.js";
import {
loadDaemonConfig,
getDaemonPaths,
writeDaemonStatus
} from "./daemon-config.js";
import { DaemonContextService } from "./services/context-service.js";
import { DaemonLinearService } from "./services/linear-service.js";
import { DaemonMaintenanceService } from "./services/maintenance-service.js";
import { DaemonMemoryService } from "./services/memory-service.js";
class UnifiedDaemon {
config;
paths;
contextService;
linearService;
maintenanceService;
memoryService;
heartbeatInterval;
isShuttingDown = false;
startTime = 0;
constructor(config) {
this.paths = getDaemonPaths();
this.config = { ...loadDaemonConfig(), ...config };
this.contextService = new DaemonContextService(
this.config.context,
(level, msg, data) => this.log(level, "context", msg, data)
);
this.linearService = new DaemonLinearService(
this.config.linear,
(level, msg, data) => this.log(level, "linear", msg, data)
);
this.maintenanceService = new DaemonMaintenanceService(
this.config.maintenance,
(level, msg, data) => this.log(level, "maintenance", msg, data)
);
this.memoryService = new DaemonMemoryService(
this.config.memory,
(level, msg, data) => this.log(level, "memory", msg, data)
);
}
log(level, service, message, data) {
const logLevel = level.toUpperCase();
const levels = ["DEBUG", "INFO", "WARN", "ERROR"];
const configLevel = this.config.logLevel.toUpperCase();
if (levels.indexOf(logLevel) < levels.indexOf(configLevel)) {
return;
}
const entry = {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
level: logLevel,
service,
message,
data
};
const logLine = JSON.stringify(entry) + "\n";
try {
appendFileSync(this.paths.logFile, logLine);
} catch {
console.error(`[${entry.timestamp}] ${level} [${service}]: ${message}`);
}
}
checkIdempotency() {
if (existsSync(this.paths.pidFile)) {
try {
const existingPid = readFileSync(this.paths.pidFile, "utf8").trim();
const pid = parseInt(existingPid, 10);
if (isProcessAlive(pid)) {
this.log("WARN", "daemon", "Daemon already running", { pid });
return false;
}
this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
unlinkSync(this.paths.pidFile);
} catch {
try {
unlinkSync(this.paths.pidFile);
} catch {
}
}
}
return true;
}
writePidFile() {
writeFileSync(this.paths.pidFile, process.pid.toString());
this.log("INFO", "daemon", "PID file created", {
pid: process.pid,
file: this.paths.pidFile
});
}
updateStatus() {
const maintenanceState = this.maintenanceService.getState();
const memoryState = this.memoryService.getState();
const status = {
running: true,
pid: process.pid,
startTime: this.startTime,
uptime: Date.now() - this.startTime,
services: {
context: {
enabled: this.config.context.enabled,
lastRun: this.contextService.getState().lastSaveTime || void 0,
saveCount: this.contextService.getState().saveCount
},
linear: {
enabled: this.config.linear.enabled,
lastRun: this.linearService.getState().lastSyncTime || void 0,
syncCount: this.linearService.getState().syncCount
},
maintenance: {
enabled: this.config.maintenance.enabled,
lastRun: maintenanceState.lastRunTime || void 0,
staleFramesCleaned: maintenanceState.staleFramesCleaned,
ftsRebuilds: maintenanceState.ftsRebuilds,
embeddingsGenerated: maintenanceState.embeddingsGenerated,
embeddingsTotal: maintenanceState.embeddingsTotal,
embeddingsRemaining: maintenanceState.embeddingsRemaining
},
memory: {
enabled: this.config.memory.enabled,
lastTrigger: memoryState.lastTriggerTime || void 0,
triggerCount: memoryState.triggerCount,
currentRamPercent: memoryState.currentRamPercent
},
fileWatch: {
enabled: this.config.fileWatch.enabled
}
},
errors: [
...this.contextService.getState().errors.slice(-5),
...this.linearService.getState().errors.slice(-5),
...maintenanceState.errors.slice(-5),
...memoryState.errors.slice(-5)
]
};
writeDaemonStatus(status);
}
setupSignalHandlers() {
const handleSignal = (signal) => {
this.log("INFO", "daemon", `Received ${signal}, shutting down`);
this.shutdown(signal.toLowerCase());
};
process.on("SIGTERM", () => handleSignal("SIGTERM"));
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGHUP", () => handleSignal("SIGHUP"));
process.on("uncaughtException", (err) => {
this.log("ERROR", "daemon", "Uncaught exception", {
error: err.message,
stack: err.stack
});
this.shutdown("uncaught_exception");
});
process.on("unhandledRejection", (reason) => {
this.log("ERROR", "daemon", "Unhandled rejection", {
reason: String(reason)
});
});
}
cleanup() {
try {
if (existsSync(this.paths.pidFile)) {
unlinkSync(this.paths.pidFile);
this.log("INFO", "daemon", "PID file removed");
}
} catch (e) {
this.log("WARN", "daemon", "Failed to remove PID file", {
error: String(e)
});
}
const finalStatus = {
running: false,
startTime: this.startTime,
uptime: Date.now() - this.startTime,
services: {
context: {
enabled: false,
saveCount: this.contextService.getState().saveCount
},
linear: {
enabled: false,
syncCount: this.linearService.getState().syncCount
},
maintenance: {
enabled: false,
staleFramesCleaned: this.maintenanceService.getState().staleFramesCleaned,
ftsRebuilds: this.maintenanceService.getState().ftsRebuilds
},
memory: {
enabled: false,
triggerCount: this.memoryService.getState().triggerCount
},
fileWatch: { enabled: false }
},
errors: []
};
writeDaemonStatus(finalStatus);
}
shutdown(reason) {
if (this.isShuttingDown) return;
this.isShuttingDown = true;
this.log("INFO", "daemon", "Daemon shutting down", {
reason,
uptime: Date.now() - this.startTime,
contextSaves: this.contextService.getState().saveCount,
linearSyncs: this.linearService.getState().syncCount,
maintenanceRuns: this.maintenanceService.getState().ftsRebuilds,
memoryTriggers: this.memoryService.getState().triggerCount
});
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = void 0;
}
this.contextService.stop();
this.linearService.stop();
this.maintenanceService.stop();
this.memoryService.stop();
this.cleanup();
const exitCode = reason === "sigterm" || reason === "sigint" || reason === "sighup" ? 0 : 1;
process.exit(exitCode);
}
async start() {
if (!this.checkIdempotency()) {
this.log("INFO", "daemon", "Exiting - daemon already running");
process.exit(0);
}
this.startTime = Date.now();
this.writePidFile();
this.setupSignalHandlers();
this.log("INFO", "daemon", "Unified daemon started", {
pid: process.pid,
config: {
context: this.config.context.enabled,
linear: this.config.linear.enabled,
maintenance: this.config.maintenance.enabled,
memory: this.config.memory.enabled,
fileWatch: this.config.fileWatch.enabled
}
});
this.contextService.start();
await this.linearService.start();
this.maintenanceService.start();
this.memoryService.start();
this.heartbeatInterval = setInterval(() => {
this.updateStatus();
}, this.config.heartbeatInterval * 1e3);
this.updateStatus();
}
getStatus() {
const maintenanceState = this.maintenanceService.getState();
const memoryState = this.memoryService.getState();
return {
running: !this.isShuttingDown,
pid: process.pid,
startTime: this.startTime,
uptime: Date.now() - this.startTime,
services: {
context: {
enabled: this.config.context.enabled,
lastRun: this.contextService.getState().lastSaveTime || void 0,
saveCount: this.contextService.getState().saveCount
},
linear: {
enabled: this.config.linear.enabled,
lastRun: this.linearService.getState().lastSyncTime || void 0,
syncCount: this.linearService.getState().syncCount
},
maintenance: {
enabled: this.config.maintenance.enabled,
lastRun: maintenanceState.lastRunTime || void 0,
staleFramesCleaned: maintenanceState.staleFramesCleaned,
ftsRebuilds: maintenanceState.ftsRebuilds,
embeddingsGenerated: maintenanceState.embeddingsGenerated,
embeddingsTotal: maintenanceState.embeddingsTotal,
embeddingsRemaining: maintenanceState.embeddingsRemaining
},
memory: {
enabled: this.config.memory.enabled,
lastTrigger: memoryState.lastTriggerTime || void 0,
triggerCount: memoryState.triggerCount,
currentRamPercent: memoryState.currentRamPercent
},
fileWatch: {
enabled: this.config.fileWatch.enabled
}
},
errors: [
...this.contextService.getState().errors,
...this.linearService.getState().errors,
...maintenanceState.errors,
...memoryState.errors
]
};
}
}
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("unified-daemon.js")) {
const args = process.argv.slice(2);
const config = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--save-interval" && args[i + 1]) {
config.context = { enabled: true, interval: parseInt(args[i + 1], 10) };
i++;
} else if (arg === "--linear-interval" && args[i + 1]) {
config.linear = {
enabled: true,
interval: parseInt(args[i + 1], 10),
retryAttempts: 3,
retryDelay: 3e4
};
i++;
} else if (arg === "--no-linear") {
config.linear = {
enabled: false,
interval: 60,
retryAttempts: 3,
retryDelay: 3e4
};
} else if (arg === "--log-level" && args[i + 1]) {
config.logLevel = args[i + 1];
i++;
}
}
const daemon = new UnifiedDaemon(config);
daemon.start().catch((err) => {
console.error("Failed to start daemon:", err);
process.exit(1);
});
}
export {
UnifiedDaemon
};