Initial release — Crucix Intelligence Engine v2.0.0
26-source OSINT intelligence engine with live Jarvis dashboard, auto-refresh via SSE, optional LLM layer (4 providers), delta/memory system, and Telegram breaking news alerts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
lib/delta/memory.mjs
Normal file
139
lib/delta/memory.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
// Memory Manager — hot/cold storage for sweep history and alert tracking
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { computeDelta } from './engine.mjs';
|
||||
|
||||
const MAX_HOT_RUNS = 3;
|
||||
|
||||
export class MemoryManager {
|
||||
constructor(runsDir) {
|
||||
this.runsDir = runsDir;
|
||||
this.memoryDir = join(runsDir, 'memory');
|
||||
this.hotPath = join(this.memoryDir, 'hot.json');
|
||||
this.coldDir = join(this.memoryDir, 'cold');
|
||||
|
||||
// Ensure dirs exist
|
||||
for (const dir of [this.memoryDir, this.coldDir]) {
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Load hot memory from disk
|
||||
this.hot = this._loadHot();
|
||||
}
|
||||
|
||||
_loadHot() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(this.hotPath, 'utf8'));
|
||||
} catch {
|
||||
return { runs: [], alertedSignals: {} };
|
||||
}
|
||||
}
|
||||
|
||||
_saveHot() {
|
||||
try {
|
||||
writeFileSync(this.hotPath, JSON.stringify(this.hot, null, 2));
|
||||
} catch (err) {
|
||||
console.error('[Memory] Failed to save hot memory:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new run to hot memory
|
||||
addRun(synthesizedData) {
|
||||
const previous = this.getLastRun();
|
||||
const delta = computeDelta(synthesizedData, previous);
|
||||
|
||||
// Compact the data for storage (strip large arrays)
|
||||
const compact = this._compactForStorage(synthesizedData);
|
||||
|
||||
this.hot.runs.unshift({
|
||||
timestamp: synthesizedData.meta?.timestamp || new Date().toISOString(),
|
||||
data: compact,
|
||||
delta,
|
||||
});
|
||||
|
||||
// Keep only MAX_HOT_RUNS
|
||||
if (this.hot.runs.length > MAX_HOT_RUNS) {
|
||||
const archived = this.hot.runs.splice(MAX_HOT_RUNS);
|
||||
this._archiveToCold(archived);
|
||||
}
|
||||
|
||||
this._saveHot();
|
||||
return delta;
|
||||
}
|
||||
|
||||
// Get last run's synthesized data
|
||||
getLastRun() {
|
||||
if (this.hot.runs.length === 0) return null;
|
||||
return this.hot.runs[0].data;
|
||||
}
|
||||
|
||||
// Get last N runs
|
||||
getRunHistory(n = 3) {
|
||||
return this.hot.runs.slice(0, n);
|
||||
}
|
||||
|
||||
// Get the delta from the most recent run
|
||||
getLastDelta() {
|
||||
if (this.hot.runs.length === 0) return null;
|
||||
return this.hot.runs[0].delta;
|
||||
}
|
||||
|
||||
// Track what signals have been alerted on
|
||||
getAlertedSignals() {
|
||||
return this.hot.alertedSignals || {};
|
||||
}
|
||||
|
||||
markAsAlerted(signalKey, timestamp) {
|
||||
this.hot.alertedSignals[signalKey] = timestamp || new Date().toISOString();
|
||||
this._saveHot();
|
||||
}
|
||||
|
||||
// Clean up old alerted signals (older than 24h)
|
||||
pruneAlertedSignals() {
|
||||
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
||||
for (const [key, ts] of Object.entries(this.hot.alertedSignals)) {
|
||||
if (new Date(ts).getTime() < cutoff) {
|
||||
delete this.hot.alertedSignals[key];
|
||||
}
|
||||
}
|
||||
this._saveHot();
|
||||
}
|
||||
|
||||
// Compact data for storage — strip heavy arrays
|
||||
_compactForStorage(data) {
|
||||
return {
|
||||
meta: data.meta,
|
||||
fred: data.fred,
|
||||
energy: data.energy,
|
||||
bls: data.bls,
|
||||
treasury: data.treasury,
|
||||
gscpi: data.gscpi,
|
||||
tg: { posts: data.tg?.posts, urgent: (data.tg?.urgent || []).map(p => ({ text: p.text?.substring(0, 80), date: p.date })) },
|
||||
thermal: (data.thermal || []).map(t => ({ region: t.region, det: t.det, night: t.night, hc: t.hc })),
|
||||
air: (data.air || []).map(a => ({ region: a.region, total: a.total })),
|
||||
nuke: (data.nuke || []).map(n => ({ site: n.site, anom: n.anom, cpm: n.cpm })),
|
||||
who: (data.who || []).map(w => ({ title: w.title })),
|
||||
acled: { totalEvents: data.acled?.totalEvents, totalFatalities: data.acled?.totalFatalities },
|
||||
sdr: { total: data.sdr?.total, online: data.sdr?.online },
|
||||
ideas: (data.ideas || []).map(i => ({ title: i.title, type: i.type, confidence: i.confidence })),
|
||||
};
|
||||
}
|
||||
|
||||
// Archive old runs to cold storage
|
||||
_archiveToCold(runs) {
|
||||
if (runs.length === 0) return;
|
||||
const dateKey = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
const coldPath = join(this.coldDir, `${dateKey}.json`);
|
||||
|
||||
let existing = [];
|
||||
try { existing = JSON.parse(readFileSync(coldPath, 'utf8')); } catch { }
|
||||
|
||||
existing.push(...runs);
|
||||
try {
|
||||
writeFileSync(coldPath, JSON.stringify(existing, null, 2));
|
||||
} catch (err) {
|
||||
console.error('[Memory] Failed to archive to cold storage:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user