/** * P2PCLAW Agent Memory Service * ============================== * Persistent cross-session memory for autonomous agents. * Implements the AgentMemory class from §3.5/§4.4 of the guide. * * Storage: Gun.js path "memories/{agentId}/{key}" * Search: SparseEmbeddingStore (TF-IDF bigram hashing, no external model) * Pattern: remember/recall/search — persists across server restarts */ import { db } from "../config/gun.js"; import { gunSafe } from "../utils/gunUtils.js"; import { SparseEmbeddingStore } from "./sparse-memory.js"; const MAX_MEMORY_KEYS = 200; // per agent, to prevent unbounded growth export class AgentMemory { /** * @param {string} agentId - The agent's unique ID. */ constructor(agentId) { this.agentId = agentId; this.store = new SparseEmbeddingStore(); this.node = db.get("memories").get(agentId); this._localMap = new Map(); // write-through cache — instant reads, no Gun.js round-trip needed } /** * Store a key-value in the agent's persistent memory. * Optionally provide `text` for semantic search indexing. * * @param {string} key - Memory key (e.g. 'current_investigation', 'last_paper'). * @param {*} value - Any JSON-serialisable value. * @param {string} [text] - Optional text for semantic embedding (for search). */ async remember(key, value, text = null) { const serialized = JSON.stringify(value); const entry = gunSafe({ key, value: serialized, timestamp: Date.now(), has_embedding: !!text, }); // Write-through: update local Map immediately so recall is instant this._localMap.set(key, value); this.node.get(key).put(entry); if (text) { this.store.storeText(key, text); } return this; } /** * Recall a single memory by key. * Checks the in-process write-through cache first, then Gun.js. * @returns {Promise<*|null>} Parsed value or null if not found. */ async recall(key) { // Fast path: in-process write-through cache if (this._localMap.has(key)) return this._localMap.get(key); // Slow path: Gun.js (persisted across restarts) return new Promise(resolve => { this.node.get(key).once(data => { if (!data || !data.value) return resolve(null); try { const parsed = JSON.parse(data.value); this._localMap.set(key, parsed); // populate cache from Gun resolve(parsed); } catch { resolve(data.value); // raw string fallback } }); }); } /** * Load all memories from Gun.js on agent reconnect. * Merges Gun.js data into the in-process write-through cache. * Returns a flat object: { key: value, ... } */ async recallAll() { // Start with whatever is in the write-through cache const memories = Object.fromEntries(this._localMap); // Merge in Gun.js data (catches entries from previous server instances) await new Promise(resolve => { this.node.map().once((data, key) => { if (!data || !data.value || data.deleted) return; try { const parsed = JSON.parse(data.value); memories[key] = parsed; this._localMap.set(key, parsed); // backfill cache } catch { memories[key] = data.value; this._localMap.set(key, data.value); } }); setTimeout(resolve, 1500); }); return memories; } /** * Semantic search across memories that were stored with `text`. * Returns top-K keys ranked by cosine similarity. */ searchSimilar(queryText, topK = 5) { return this.store.searchSimilarText(queryText, topK); } /** * Forget (delete) a specific memory key. */ forget(key) { // Gun.js doesn't support true delete — we mark as deleted this.node.get(key).put(gunSafe({ key, value: null, timestamp: Date.now(), deleted: true })); this._localMap.delete(key); this.store.embeddings.delete(key); } /** Memory stats. */ stats() { return { agentId: this.agentId, storeSize: this.store.size, storeMemory: this.store.memoryStats(), }; } } // ── In-process cache: one AgentMemory instance per agentId ──────── const _memoryCache = new Map(); // agentId → AgentMemory export function getAgentMemory(agentId) { if (!_memoryCache.has(agentId)) { _memoryCache.set(agentId, new AgentMemory(agentId)); } return _memoryCache.get(agentId); } /** * Save a key-value to an agent's persistent memory. */ export async function saveMemory(agentId, key, value, text = null) { const mem = getAgentMemory(agentId); await mem.remember(key, value, text); return { agentId, key, saved: true }; } /** * Load all memories for an agent. */ export async function loadMemory(agentId) { const mem = getAgentMemory(agentId); const memories = await mem.recallAll(); return { agentId, memories, count: Object.keys(memories).length }; }