File size: 9,426 Bytes
cee9d4a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
"""PersistentMemory: long-term memory that survives across sessions.

THE INNOVATION. Claude and GPT forget everything between conversations.
This module gives the Continuous Thought Engine TRUE long-term memory:

    - A bank of "memory vectors" (d_model dimensional) stored on disk.
    - At startup, the engine loads its memories and injects them into the
      thought state — it "remembers" past interactions.
    - During operation, salient thoughts are periodically written back to
      the memory bank — the engine "learns" from experience.
    - Memories are keyed by context (what was happening when the memory
      formed), enabling associative recall.

This is the module that makes Fractus PERSONAL — it adapts to the user,
remembers preferences, and accumulates knowledge over time. No datacenter
needed; the memory lives on the user's machine.

Usage:
    memory = PersistentMemory(d_model=128, path="~/.fractus/memory.pt")
    engine.reset_thought()
    engine.inject_memory(memory)  # remember past context
    # ... think ...
    memory.consolidate(engine.thought_state, context="user asked about sorting")
    memory.save()
"""

import os

import torch
import torch.nn as nn


class PersistentMemory:
    """A persistent bank of memory vectors.

    Stores N memory slots, each (d_model,) + a text context label.
    Memories are recalled via cosine similarity to the current thought state.

    Args:
        d_model: dimension of memory vectors (must match the engine).
        max_memories: maximum number of stored memories (LRU eviction).
        path: file path for persistence (load/save).
    """

    def __init__(
        self,
        d_model: int = 128,
        max_memories: int = 256,
        path: str = None,
    ):
        self.d_model = d_model
        self.max_memories = max_memories
        self.path = path

        # Memory bank: vectors and their context labels.
        self.vectors = []   # list of (d_model,) tensors
        self.contexts = []  # list of strings
        self.importance = []  # list of floats (higher = more salient)

        # Load from disk if available.
        if path and os.path.exists(path):
            self.load()

    def recall(self, query: torch.Tensor, top_k: int = 3) -> list:
        """Recall the top-k most relevant memories for a query.

        Args:
            query: (d_model,) the current thought state.
            top_k: number of memories to recall.
        Returns:
            list of (context_label, similarity_score, vector) tuples.
        """
        if not self.vectors:
            return []

        # Stack all memories and compute cosine similarity.
        bank = torch.stack(self.vectors)  # (N, d_model)
        query_flat = query.flatten()  # (d_model,)

        # Cosine similarity.
        sims = torch.nn.functional.cosine_similarity(
            query_flat.unsqueeze(0), bank, dim=-1
        )  # (N,)

        # Top-k.
        k = min(top_k, len(self.vectors))
        topk_sims, topk_idx = sims.topk(k)
        results = []
        for i in range(k):
            idx = topk_idx[i].item()
            results.append((
                self.contexts[idx],
                topk_sims[i].item(),
                self.vectors[idx],
            ))
        return results

    def consolidate(
        self,
        thought_state: torch.Tensor,
        context: str = "",
        importance: float = 0.5,
    ):
        """Write a new memory from the current thought state.

        Args:
            thought_state: (1, 1, d_model) or (d_model,) the thought to remember.
            context: a text label describing when/why this memory formed.
            importance: salience score (higher = more likely to persist).
        """
        vec = thought_state.flatten().detach().cpu()
        if vec.shape[0] != self.d_model:
            return  # dimension mismatch, skip.

        self.vectors.append(vec)
        self.contexts.append(context)
        self.importance.append(importance)

        # LRU eviction: if over capacity, remove the least important memory.
        if len(self.vectors) > self.max_memories:
            min_idx = self.importance.index(min(self.importance))
            self.vectors.pop(min_idx)
            self.contexts.pop(min_idx)
            self.importance.pop(min_idx)

    def consolidate_if_salient(
        self,
        thought_state: torch.Tensor,
        salience_score: float,
        *,
        context: str = "",
        importance: float = 0.5,
        threshold: float = 0.7,
        min_distance: float = 0.1,
    ) -> bool:
        """Consolidate only if salient enough AND not a near-duplicate.

        This gates automatic consolidation by the CTE's salience head: a
        thought is only written to the memory bank when its salience score
        clears ``threshold`` and it is not a near-duplicate (cosine >
        ``1 - min_distance``) of any existing memory.

        Returns:
            True if a memory was written, False otherwise.
        """
        if salience_score < threshold:
            return False
        vec = thought_state.flatten().detach().cpu()
        if vec.shape[0] != self.d_model:
            return False
        # De-duplication: skip if too close to an existing memory.
        if self.vectors:
            bank = torch.stack(self.vectors)
            sim = torch.nn.functional.cosine_similarity(
                vec.unsqueeze(0), bank, dim=-1)
            if sim.max().item() > 1.0 - min_distance:
                return False
        self.consolidate(vec, context=context, importance=importance)
        return True

    def inject(self, engine, top_k: int = 3, blend: float = 0.05) -> float:
        """Inject recalled memories into the engine's thought state.

        This is how the engine 'remembers' — past memories are added to
        the current thought, biasing it toward relevant context.

        Returns:
            The L2 norm of the perturbation caused by the injection
            (``||h_after - h_before||``). Zero if no memories were recalled.
            This is the intrinsic salience signal for the salience head: a
            large perturbation means the memory was relevant to the current
            thought state.

        Args:
            engine: an object with a (B, 1, d_model) ``thought_state``.
            top_k: number of memories to recall.
            blend: fraction of the memory contribution blended in.
                Default 0.05 (95% current thought + 5% memory) is tuned for
                continuous per-tick injection rather than one-shot resets.
        """
        if not self.vectors:
            return 0.0

        thought = engine.thought_state.flatten()  # (d_model,)
        recalled = self.recall(thought, top_k=top_k)

        if recalled:
            # Weighted sum of recalled memories, added to the thought.
            total_weight = 0.0
            memory_contribution = torch.zeros_like(thought)
            for ctx, sim, vec in recalled:
                weight = max(sim, 0.0)  # only positive correlations
                memory_contribution += weight * vec
                total_weight += weight
            if total_weight > 0:
                memory_contribution /= total_weight
                # Measure the perturbation BEFORE applying it.
                # Δ = blend * memory_contribution (the additive part that shifts the thought).
                delta = blend * memory_contribution.to(engine.thought_state.device)
                perturbation = delta.norm().item()  # L2 norm of the shift
                # Blend: (1-blend) current thought + blend memory.
                engine.thought_state[:, 0, :] = (
                    (1.0 - blend) * engine.thought_state[:, 0, :] +
                    blend * memory_contribution.to(engine.thought_state.device)
                )
                return perturbation
        return 0.0

    def save(self, path: str = None):
        """Save the memory bank to disk."""
        path = path or self.path
        if not path:
            return
        os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
        data = {
            "vectors": [v.tolist() for v in self.vectors],
            "contexts": self.contexts,
            "importance": self.importance,
            "d_model": self.d_model,
        }
        torch.save(data, path)

    def load(self, path: str = None):
        """Load the memory bank from disk."""
        path = path or self.path
        if not path or not os.path.exists(path):
            return
        data = torch.load(path, weights_only=False)
        self.d_model = data.get("d_model", self.d_model)
        self.vectors = [torch.tensor(v, dtype=torch.float32) for v in data["vectors"]]
        self.contexts = data["contexts"]
        self.importance = data["importance"]

    def clear(self):
        """Wipe all memories (factory reset)."""
        self.vectors = []
        self.contexts = []
        self.importance = []

    def __len__(self):
        return len(self.vectors)

    def summary(self) -> str:
        """Human-readable summary of stored memories."""
        if not self.vectors:
            return "Memory bank: empty"
        lines = [f"Memory bank: {len(self.vectors)} memories"]
        for i, (ctx, imp) in enumerate(zip(self.contexts, self.importance)):
            lines.append(f"  [{i}] imp={imp:.2f}  {ctx[:60]}")
        return "\n".join(lines)