From 2538c91876455fe96febf441fc8dde20f2606023 Mon Sep 17 00:00:00 2001 From: Agando Date: Thu, 9 Jul 2026 06:00:32 +0200 Subject: auto: 2026-07-09_06:00 changes --- modules/core/module-memory-graph.js | 303 +++++++++++++++++++++++++++++++++--- 1 file changed, 283 insertions(+), 20 deletions(-) (limited to 'modules/core/module-memory-graph.js') diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js index 2382a4a..ffc79e0 100644 --- a/modules/core/module-memory-graph.js +++ b/modules/core/module-memory-graph.js @@ -12,6 +12,11 @@ class MemoryGraphModule { this._debounceTimer = null; this._initialized = false; this._totalNodeAccesses = 0; + // v4.5.89: Decay configuration + this._decayDays = 30; + this._decayMinUses = 3; + this._decayRate = 0.01; // Daily decay rate + this._lastDecayRun = 0; } getHelpInfo() { @@ -21,6 +26,10 @@ class MemoryGraphModule { 'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info', 'memgraph-export': 'memgraph-export — Export graph as JSON', 'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)', + 'memgraph-learn': 'memgraph-learn — Add a fact to the memory graph', + 'memgraph-session': 'memgraph-session [date] — Show graph nodes for a session (default: today)', + 'memgraph-agent': 'memgraph-agent — Show namespaced nodes for an agent', + 'memgraph-decay-config': 'memgraph-decay-config — Configure decay policy (default: 30d, 3 uses)', }; } @@ -30,6 +39,11 @@ class MemoryGraphModule { this.commands['memgraph-stats'] = () => this.showStats(); this.commands['memgraph-export'] = () => this.exportGraph(); this.commands['memgraph-prune'] = () => this.prune(); + this.commands['memgraph-learn'] = (args) => this.learn(args); + this.commands['memgraph-session'] = (args) => this.showSession(args.join(' ')); + this.commands['memgraph-agent'] = (args) => this.showAgent(args.join(' ')); + // v4.5.89: Decay configuration + this.commands['memgraph-decay-config'] = (args) => this.decayConfig(args); } init(terminal) { @@ -38,13 +52,16 @@ class MemoryGraphModule { this._initialized = true; this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info'); // Log startup for graph - this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`); + // v4.5.89 fix: Use day-floored key to match edge source keys (P0-1 from review) + this.addNode('session', `session-${Math.floor(Date.now() / 86400000)}`, `Session ${new Date().toLocaleDateString()}`); } // ─── Node API ─── - addNode(type, id, label, metadata = {}) { - const key = `${type}:${id}`; + addNode(type, id, label, metadata = {}, agentId = null) { + // v4.5.88: Support agent namespacing with dual-lookup + const ns = agentId ? `agent:${agentId}` : 'global'; + const key = agentId ? `${ns}:${type}:${id}` : `${type}:${id}`; if (!this._nodes[key]) { this._nodes[key] = { id: key, @@ -53,7 +70,9 @@ class MemoryGraphModule { metadata: metadata || {}, created: new Date().toISOString(), updated: new Date().toISOString(), - access_count: 1 + access_count: 1, + namespace: ns, + source_session: `session-${Math.floor(Date.now() / 86400000)}` }; } else { this._nodes[key].updated = new Date().toISOString(); @@ -67,8 +86,14 @@ class MemoryGraphModule { return key; } - getNode(type, id) { - return this._nodes[`${type}:${id}`] || null; + // v4.5.88: Dual-lookup — try namespaced key first, fall back to global + getNode(type, id, agentId = null) { + if (agentId) { + const nsKey = `agent:${agentId}:${type}:${id}`; + if (this._nodes[nsKey]) return this._nodes[nsKey]; + } + const globalKey = `${type}:${id}`; + return this._nodes[globalKey] || null; } findNodes(query) { @@ -82,7 +107,8 @@ class MemoryGraphModule { // ─── Edge API ─── - addEdge(sourceKey, targetKey, type, weight = 1) { + // v4.5.88: addEdge tracks source_agents for multi-agent attribution + addEdge(sourceKey, targetKey, type, weight = 1, agentId = null) { // Check if edge already exists let edge = this._edges.find(e => e.source === sourceKey && e.target === targetKey && e.type === type @@ -91,6 +117,7 @@ class MemoryGraphModule { edge.weight = (edge.weight + weight) / 2; edge.last_used = new Date().toISOString(); edge.use_count++; + if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])]; } else { this._edges.push({ source: sourceKey, @@ -99,7 +126,8 @@ class MemoryGraphModule { weight, created: new Date().toISOString(), last_used: new Date().toISOString(), - use_count: 1 + use_count: 1, + source_agents: agentId ? [agentId] : [] }); } this._debounceSave(); @@ -107,11 +135,12 @@ class MemoryGraphModule { // ─── Event-based Learning ─── - recordFileAccess(path) { + // v4.5.89 fix: Thread agentId through record* methods (P1-3 from review) + recordFileAccess(path, agentId = null) { const name = path.split('/').pop() || path; - const nodeKey = this.addNode('file', path, name); + const nodeKey = this.addNode('file', path, name, {}, agentId); const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`; - this.addEdge(sessionKey, nodeKey, 'accessed', 0.5); + this.addEdge(sessionKey, nodeKey, 'accessed', 0.5, agentId); // Extract file extension edge const ext = path.split('.').pop(); @@ -121,10 +150,10 @@ class MemoryGraphModule { } } - recordToolCall(toolName, success) { - const nodeKey = this.addNode('tool', toolName, toolName); + recordToolCall(toolName, success, agentId = null) { + const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId); const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`; - this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8); + this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId); } recordConcept(concept, filePath) { @@ -134,15 +163,16 @@ class MemoryGraphModule { this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7); } + // v4.5.88: recordTask now passes agentId for namespacing + adds source_session link recordTask(task, result, agentId) { const taskId = `task-${Date.now()}`; const taskKey = this.addNode('task', taskId, task.substring(0, 60), { full: task, result: result?.substring(0, 100), agent: agentId - }); + }, agentId); const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`; - this.addEdge(sessionKey, taskKey, 'executed', 0.6); + this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId); // Extract keywords as concepts const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3); @@ -201,18 +231,38 @@ class MemoryGraphModule { } } - return lines.join('\\n'); + return lines.join('\n'); } getContextForPrompt() { const summary = this.getContextSummary(); if (!summary) return ''; - return `[Memory Graph Context]\\n${summary}\\n`; + // Suppress noise: don't inject when graph is nearly empty + const totalNodes = Object.keys(this._nodes).length; + const totalEdges = this._edges.length; + if (totalNodes <= 2 && totalEdges === 0) return ''; + return `[Memory Graph Context]\n${summary}\n`; + } + + // Alias for orchestrator compatibility (orchestrator calls getStats, not showStats) + getStats() { + const types = {}; + for (const n of Object.values(this._nodes)) { + types[n.type] = (types[n.type] || 0) + 1; + } + return { + nodes: Object.keys(this._nodes).length, + edges: this._edges.length, + types: types, + totalAccesses: this._totalNodeAccesses + }; } // ─── Commands ─── showStatus() { + // v4.5.89: Lazy decay — run applyDecay() on status check + this.applyDecay(); const types = {}; for (const n of Object.values(this._nodes)) { types[n.type] = (types[n.type] || 0) + 1; @@ -227,6 +277,8 @@ class MemoryGraphModule { this.terminal.print(` Edges: ${this._edges.length}`, 'info'); this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info'); this.terminal.print(` Path: ${this._indexPath}`, 'info'); + // v4.5.89: Show decay config + this.terminal.print(` Decay: ${this._decayDays}d threshold, ${this._decayMinUses} min uses, rate ${this._decayRate}/day`, 'text-muted'); } search(query) { @@ -346,6 +398,181 @@ class MemoryGraphModule { this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info'); } + // v4.5.89: Apply decay scores to nodes based on age and access patterns + applyDecay() { + const now = Date.now(); + if (now - this._lastDecayRun < 3600000) return; + this._lastDecayRun = now; + + // v4.5.89b fix: Compute decay rate from _decayDays (P0 from review) + const effectiveRate = 0.9 / this._decayDays; + let decayed = 0; + let removed = 0; + + for (const key of Object.keys(this._nodes)) { + const node = this._nodes[key]; + const ageDays = (now - new Date(node.updated || node.created).getTime()) / 86400000; + node.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate)); + if (node.decay_score < 0.1 && node.access_count < this._decayMinUses) { + if (node.type === 'session') continue; + if (node.namespace && node.namespace.startsWith('agent:')) continue; + delete this._nodes[key]; + removed++; + continue; + } + decayed++; + } + + this._edges = this._edges.filter(e => { + const ageDays = (now - new Date(e.last_used).getTime()) / 86400000; + e.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate)); + return !(e.decay_score < 0.1 && (e.use_count || 1) < this._decayMinUses); + }); + + if (removed > 0) { + this._debounceSave(); + console.log('[MemoryGraph] Decay: ' + decayed + ' scored, ' + removed + ' auto-removed'); + } + + // v4.5.90: Check for stale skills + var staleSkills = this.getStaleSkills(); + if (staleSkills.length > 0) { + this.terminal.print('🧠 Stale skills: ' + staleSkills.length + ' (' + staleSkills.map(function(s) { return s.name; }).join(', ') + ')', 'warning'); + } + } + + // v4.5.90: Record skill usage for decay tracking + recordSkillUse(name, success) { + try { + const fs = this.terminal.fs; + if (!fs) return; + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (!index[name]) index[name] = { useCount: 0, successCount: 0 }; + index[name].lastUsed = new Date().toISOString(); + index[name].useCount = (index[name].useCount || 0) + 1; + if (success !== false) { + index[name].successCount = (index[name].successCount || 0) + 1; + } + fs.fsWriteJSON('/skills/index.json', index); + } catch (e) { + console.warn('[MemoryGraph] Skill use tracking error:', e.message); + } + } + + // v4.5.90: Get stale skills (unused >_decayDays, useCount == 0, not immune) + getStaleSkills() { + try { + const fs = this.terminal.fs; + if (!fs) return []; + const index = fs.fsReadJSON('/skills/index.json') || {}; + const now = Date.now(); + // v4.5.90b fix: Use configurable _decayDays (P1 from review) + const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000; + var stale = []; + for (var name of Object.keys(index)) { + var stats = index[name] || {}; + if (stats.immune) continue; + var useCount = stats.useCount || 0; + var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0; + if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) { + var daysUnused = lastUsed > 0 ? Math.floor((now - lastUsed) / 86400000) : 999; + stale.push({ name: name, daysUnused: daysUnused }); + } + } + return stale; + } catch (e) { + return []; + } + } + + // v4.5.89: Configure decay policy + decayConfig(args) { + if (args.length < 2) { + this.terminal.print('Usage: memgraph-decay-config ', 'error'); + this.terminal.print(' Current: ' + this._decayDays + 'd threshold, ' + this._decayMinUses + ' min uses', 'info'); + this.terminal.print(' Decay rate: ' + this._decayRate + '/day (0.01 = 1% per day)', 'info'); + return; + } + const days = parseInt(args[0]); + const minUses = parseInt(args[1]); + if (isNaN(days) || days < 1) return this.terminal.print('Days must be >= 1', 'error'); + if (isNaN(minUses) || minUses < 0) return this.terminal.print('Min uses must be >= 0', 'error'); + this._decayDays = days; + this._decayMinUses = minUses; + this._debounceSave(); + this.terminal.print('✅ Decay config: ' + days + 'd threshold, ' + minUses + ' min uses', 'success'); + } + + learn(args) { + if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn ', 'error'); + const entity = args[0]; + const fact = args.slice(1).join(' '); + this.addNode('concept', entity, entity); + const factKey = this.addNode('fact', entity + '-' + Date.now(), fact); + const conceptKey = `concept:${entity}`; + this.addEdge(conceptKey, factKey, 'has_fact', 0.9); + this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success'); + // v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1)) + const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule']; + if (nvfs && typeof nvfs.indexGraphFacts === 'function') { + nvfs.indexSingleFact(factKey, entity, fact); + } + } + + // v4.5.88: Show graph nodes for a specific session + showSession(dateStr) { + let sessionKey; + if (dateStr) { + const d = new Date(dateStr); + if (isNaN(d.getTime())) return this.terminal.print('Usage: memgraph-session [YYYY-MM-DD]', 'error'); + sessionKey = `session-${Math.floor(d.getTime() / 86400000)}`; + } else { + sessionKey = `session-${Math.floor(Date.now() / 86400000)}`; + } + + const sessionNodes = Object.values(this._nodes).filter(function(n) { + return n.source_session === sessionKey || n.type === 'session' && n.id.includes(sessionKey); + }); + + this.terminal.print('📅 Session Graph: ' + sessionKey, 'header'); + if (sessionNodes.length === 0) { + this.terminal.print(' No nodes found for this session', 'info'); + return; + } + this.terminal.print(' ' + sessionNodes.length + ' nodes:', 'info'); + for (var n of sessionNodes.slice(0, 15)) { + var ns = n.namespace || 'global'; + this.terminal.print(' ' + n.type + ':' + n.label + ' (' + ns + ', ' + n.access_count + 'x)', 'info'); + } + var sessionEdges = this._edges.filter(function(e) { + return sessionNodes.some(function(n) { return n.id === e.source || n.id === e.target; }); + }); + this.terminal.print(' ' + sessionEdges.length + ' edges', 'info'); + } + + // v4.5.88: Show namespaced nodes for a specific agent + showAgent(agentId) { + if (!agentId) return this.terminal.print('Usage: memgraph-agent ', 'error'); + var ns = 'agent:' + agentId; + var agentNodes = Object.values(this._nodes).filter(function(n) { + return n.namespace === ns; + }); + var agentEdges = this._edges.filter(function(e) { + return (e.source_agents || []).includes(agentId); + }); + + this.terminal.print('🤖 Agent Memory: ' + agentId, 'header'); + if (agentNodes.length === 0) { + this.terminal.print(' No namespaced nodes for this agent', 'info'); + this.terminal.print(' (Agent may have used global namespace)', 'text-muted'); + return; + } + this.terminal.print(' ' + agentNodes.length + ' nodes, ' + agentEdges.length + ' edges:', 'info'); + for (var n of agentNodes.slice(0, 15)) { + this.terminal.print(' ' + n.type + ':' + n.label + ' (' + n.access_count + 'x)', 'info'); + } + } + // ─── Persistence ─── _loadFromVFS() { @@ -357,18 +584,48 @@ class MemoryGraphModule { const exportData = fs.fsReadJSON(`${this._indexPath}export.json`); if (exportData && exportData.nodes) { for (const n of exportData.nodes) { + // v4.5.89 fix: Migrate old-format nodes (P1-1 from review) + if (!n.namespace) n.namespace = 'global'; + if (!n.source_session) { + const createdMs = new Date(n.created).getTime(); + n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`; + } this._nodes[n.id] = n; } this._edges = exportData.edges || []; + for (const e of this._edges) { + if (!e.source_agents) e.source_agents = []; + } this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0; return; } // Fallback: load nodes and edges separately const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`); - if (nodes) this._nodes = nodes; + if (nodes) { + this._nodes = nodes; + for (const n of Object.values(this._nodes)) { + if (!n.namespace) n.namespace = 'global'; + if (!n.source_session) { + const createdMs = new Date(n.created).getTime(); + n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`; + } + } + } const edges = fs.fsReadJSON(`${this._indexPath}edges.json`); - if (edges) this._edges = edges; + if (edges) { + this._edges = edges; + for (const e of this._edges) { + if (!e.source_agents) e.source_agents = []; + } + } + // v4.5.89b fix: Load persisted decay config (P1 from review) + const config = fs.fsReadJSON(`${this._indexPath}config.json`); + if (config) { + if (typeof config.decayDays === 'number') this._decayDays = config.decayDays; + if (typeof config.decayMinUses === 'number') this._decayMinUses = config.decayMinUses; + if (typeof config.lastDecayRun === 'number') this._lastDecayRun = config.lastDecayRun; + } } catch (e) { console.warn('[MemoryGraph] Load error (fresh start):', e.message); } @@ -383,6 +640,12 @@ class MemoryGraphModule { } fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes); fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges); + // v4.5.89b fix: Persist decay config (P1 from review) + fs.fsWriteJSON(`${this._indexPath}config.json`, { + decayDays: this._decayDays, + decayMinUses: this._decayMinUses, + lastDecayRun: this._lastDecayRun + }); } catch (e) { console.error('[MemoryGraph] Save error:', e.message); } -- cgit v1.2.3