File size: 5,534 Bytes
e92be04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
 * 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 };
}