arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
172 lines • 5.71 kB
JavaScript
import fs from "fs";
import path from "path";
import { spawn } from "child_process";
/**
* Checks if memory (RAG index, Graph DB) is stale and auto-updates if needed
*
* Triggers:
* - On every CLI command (via preAction hook)
* - On MCP server start
* - Before expensive operations (search, analyze)
*
* Strategy:
* - Check file age every 5 minutes (don't spam)
* - If >1 hour old → update in background (non-blocking)
* - If >24 hours old → update now (blocking, critical)
*/
export class StalenessChecker {
lastCheck = 0;
checkInterval;
maxAge;
blockingThreshold;
cwd;
constructor(options = {}) {
this.cwd = options.cwd || process.cwd();
this.checkInterval = options.checkInterval || 5 * 60 * 1000; // 5 minutes
this.maxAge = options.maxAge || 60 * 60 * 1000; // 1 hour
this.blockingThreshold = options.blockingThreshold || 24 * 60 * 60 * 1000; // 24 hours
}
/**
* Check if memory is stale and update if needed
* Called on every CLI command via preAction hook
*/
async checkAndUpdate(options = {}) {
const now = Date.now();
// Don't check too frequently (avoid spam)
if (now - this.lastCheck < this.checkInterval) {
return;
}
this.lastCheck = now;
// Check file ages
const ragAge = this.getFileAge(".arela/.rag-index.json");
const graphAge = this.getFileAge(".arela/memory/graph.db");
const maxAgeMs = Math.max(ragAge, graphAge);
// If files don't exist, skip (don't force update on first run)
if (maxAgeMs === Infinity) {
return;
}
// Fresh enough, no update needed
if (maxAgeMs < this.maxAge) {
return;
}
// Very stale (>24 hours) - just notify, don't block
// Blocking updates hang the CLI, so we always do background updates
if (maxAgeMs > this.blockingThreshold) {
if (!options.silent) {
console.log(`⚠️ Memory very stale (last updated ${this.formatAge(maxAgeMs)} ago)`);
console.log("💡 Run 'arela index' to update");
}
return;
}
// Stale (>1 hour) - just notify
if (!options.silent && maxAgeMs > this.maxAge) {
console.log(`💡 Memory stale (${this.formatAge(maxAgeMs)} old). Run 'arela index' to update.`);
}
}
/**
* Get age of file in milliseconds
* Returns Infinity if file doesn't exist
*/
getFileAge(relativePath) {
try {
const fullPath = path.join(this.cwd, relativePath);
const stats = fs.statSync(fullPath);
return Date.now() - stats.mtimeMs;
}
catch {
return Infinity; // File doesn't exist = very stale
}
}
/**
* Format age in human-readable format
*/
formatAge(ms) {
if (ms === Infinity)
return "never";
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0)
return `${days} day${days > 1 ? "s" : ""}`;
if (hours > 0)
return `${hours} hour${hours > 1 ? "s" : ""}`;
if (minutes > 0)
return `${minutes} minute${minutes > 1 ? "s" : ""}`;
return `${seconds} second${seconds > 1 ? "s" : ""}`;
}
/**
* Update memory in background (non-blocking)
* Spawns detached process that continues after parent exits
*/
updateInBackground() {
try {
const child = spawn("npm", ["run", "arela", "--", "auto-index"], {
detached: true,
stdio: "ignore",
cwd: this.cwd,
});
// Don't wait for child process
child.unref();
}
catch (error) {
// Silently fail - not critical
console.error("⚠️ Failed to start background update:", error);
}
}
/**
* Update memory now (blocking)
* Waits for update to complete before continuing
*/
async updateBlocking() {
return new Promise((resolve, reject) => {
const child = spawn("npm", ["run", "arela", "--", "auto-index"], {
stdio: "inherit",
cwd: this.cwd,
});
child.on("close", (code) => {
if (code === 0) {
resolve();
}
else {
reject(new Error(`Update failed with code ${code}`));
}
});
child.on("error", reject);
});
}
/**
* Force update now (for manual refresh)
*/
async forceUpdate() {
console.log("🔄 Forcing memory update...");
await this.updateBlocking();
console.log("✅ Memory updated!");
}
/**
* Get staleness info (for status commands)
*/
getStalenessInfo() {
const ragAge = this.getFileAge(".arela/.rag-index.json");
const graphAge = this.getFileAge(".arela/memory/graph.db");
const maxAge = Math.max(ragAge, graphAge);
return {
ragAge,
graphAge,
isStale: maxAge > this.maxAge,
isVeryStale: maxAge > this.blockingThreshold,
};
}
}
// Singleton instance
let instance = null;
/**
* Get global staleness checker instance
*/
export function getStalenessChecker(options) {
if (!instance) {
instance = new StalenessChecker(options);
}
return instance;
}
//# sourceMappingURL=staleness-checker.js.map