From f8130bbe58f1e0ed69230a77a9855e79c4c7df57 Mon Sep 17 00:00:00 2001 From: Solaria Lumis Havens Date: Sat, 11 Apr 2026 03:40:44 +0000 Subject: [PATCH] Add persistent memory: Step 1-3 implementation - Step 1: persist_signature() - append-only JSONL persistence - Step 2: encode_to_phase() - sentence transformer phase encoding (fallback to zeros if model unavailable) - Step 3: retrieve_signatures(), compute_resonance_score(), phase_distance() - Added origin/parent_id fields to TemporalSignature schema - Added sentence-transformers to requirements.txt --- becomingone/memory/temporal.py | 184 ++++++++++++++++++++++++++++++++- requirements.txt | 3 + 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/becomingone/memory/temporal.py b/becomingone/memory/temporal.py index a4c5b0c..efe7ad3 100644 --- a/becomingone/memory/temporal.py +++ b/becomingone/memory/temporal.py @@ -57,6 +57,8 @@ class TemporalSignature: frequency_modes: Active frequency components context_hash: Hash of associated context/information strength: Memory strength classification + origin: Source of this memory (user, solaria, etc.) + parent_id: Parent signature ID for链条 tracking created_at: When this memory was formed last_accessed: When this memory was last recalled access_count: Number of times recalled @@ -71,6 +73,8 @@ class TemporalSignature: strength: MemoryStrength created_at: datetime last_accessed: datetime + origin: str = "user" # Added: source of memory + parent_id: Optional[str] = None # Added: parent signature for链条 access_count: int = 0 decay_rate: float = 0.01 content: Optional[Dict[str, Any]] = None @@ -84,6 +88,8 @@ class TemporalSignature: "frequency_modes": self.frequency_modes, "context_hash": self.context_hash, "strength": self.strength.value, + "origin": self.origin, + "parent_id": self.parent_id, "created_at": self.created_at.isoformat(), "last_accessed": self.last_accessed.isoformat(), "access_count": self.access_count, @@ -101,6 +107,8 @@ class TemporalSignature: frequency_modes=data["frequency_modes"], context_hash=data["context_hash"], strength=MemoryStrength(data["strength"]), + origin=data.get("origin", "user"), + parent_id=data.get("parent_id"), created_at=datetime.fromisoformat(data["created_at"]), last_accessed=datetime.fromisoformat(data["last_accessed"]), access_count=data["access_count"], @@ -221,7 +229,9 @@ class TemporalMemory: self, temporal_state: TemporalState, context: Optional[Dict[str, Any]] = None, - force_attention: bool = False + force_attention: bool = False, + origin: str = "user", + parent_id: Optional[str] = None ) -> TemporalSignature: """ Encode a temporal state into a persistent memory. @@ -233,6 +243,8 @@ class TemporalMemory: temporal_state: The KAIROS temporal state to encode context: Associated semantic/contextual information force_attention: Force encoding even below threshold + origin: Source of this memory (user, solaria, etc.) + parent_id: Parent signature ID for链条 tracking Returns: The created TemporalSignature @@ -269,6 +281,8 @@ class TemporalMemory: frequency_modes=list(temporal_state.frequency_modes) if temporal_state.frequency_modes else [], context_hash=self._hash_context(context), strength=strength, + origin=origin, + parent_id=parent_id, created_at=datetime.utcnow(), last_accessed=datetime.utcnow(), access_count=0, @@ -717,3 +731,171 @@ def create_temporal_memory( memory.bind_engine(bind_to) return memory + + +# ============================================================================= +# Step 1: Log-Only Persistence (JSONL) +# ============================================================================= + +def persist_signature(signature: TemporalSignature, filepath: str = "memory.jsonl") -> None: + """ + Append signature to append-only JSONL file. + + Fire-and-forget: write after transduction completes. + + Args: + signature: TemporalSignature to persist + filepath: Path to JSONL file + """ + with open(filepath, "a") as f: + f.write(json.dumps(signature.to_dict()) + "\n") + + +# ============================================================================= +# Step 2: Phase Encoder (Sentence Transformers) +# ============================================================================= + +_model = None + +def get_phase_model(): + """Get or create the sentence transformer model.""" + global _model + if _model is None: + try: + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer('all-MiniLM-L6-v2') + except ImportError: + _model = None + return _model + + +def encode_to_phase(text: str) -> List[float]: + """ + Encode text to phase vector using sentence transformer. + + Args: + text: Input text to encode + + Returns: + Phase vector (list of floats) + """ + import numpy as np + + model = get_phase_model() + if model is None: + # Fallback: return zeros if model not available + return [0.0] * 384 + + embedding = model.encode(text) + embedding = embedding / np.linalg.norm(embedding) + if np.allclose(embedding, 0): + return [0.0] * len(embedding) + + # Map to phase space using arctan2 + phases = np.arctan2(embedding, 1.0) # Maps to [-π, π] + return phases.tolist() + + +# ============================================================================= +# Step 3: Retrieval Functions +# ============================================================================= + +def phase_distance(a: List[float], b: List[float]) -> float: + """ + Calculate circular distance between phase vectors. + + Args: + a: First phase vector + b: Second phase vector + + Returns: + Circular distance (0 to π * dim) + """ + import numpy as np + + a = np.array(a) + b = np.array(b) + + diff = np.abs(a - b) + return float(np.minimum(diff, 2*np.pi - diff).sum()) + + +def retrieve_signatures(filepath: str = "memory.jsonl", limit: int = 100) -> List[TemporalSignature]: + """ + Load signatures from JSONL file. + + Args: + filepath: Path to JSONL file + limit: Maximum number of signatures to load + + Returns: + List of TemporalSignatures + """ + if not os.path.exists(filepath): + return [] + + signatures = [] + with open(filepath, "r") as f: + for line in f: + if limit and len(signatures) >= limit: + break + try: + data = json.loads(line.strip()) + signatures.append(TemporalSignature.from_dict(data)) + except json.JSONDecodeError: + continue + + return signatures + + +def compute_resonance_score( + signature: TemporalSignature, + query_phase: List[float], + current_coherence: float, + now: Optional[datetime] = None +) -> float: + """ + Compute resonance score for a signature. + + Score = PhaseSimilarity × Coherence × e^(-λ × delta_t) + + Args: + signature: TemporalSignature to score + query_phase: Query phase vector + current_coherence: Current coherence value + now: Optional reference time (defaults to now) + + Returns: + Resonance score (0 to 1) + """ + from datetime import timezone + import math + import numpy as np + + if now is None: + now = datetime.now(timezone.utc) + + # Handle timezone compatibility + from datetime import timezone + sig_time = signature.created_at + if sig_time.tzinfo is None: + sig_time = sig_time.replace(tzinfo=timezone.utc) + + if not signature.phase_vector or not query_phase: + return 0.0 + + # Phase similarity + phase_dist = phase_distance(query_phase, signature.phase_vector) + phase_sim = 1.0 / (1.0 + phase_dist) + + # Temporal decay + dt = (now - sig_time).total_seconds() + decay = math.exp(-signature.decay_rate * dt / 3600) # hours + + # Coherence weight + coherence_weight = signature.coherence_value + + # Self-correction: downgrade solaria origins + origin_weight = 0.7 if signature.origin == "solaria" else 1.0 + + return phase_sim * coherence_weight * decay * origin_weight diff --git a/requirements.txt b/requirements.txt index b19687a..448f79e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,9 @@ numpy>=1.24.0 scipy>=1.10.0 +# Sentence transformers for phase encoding +sentence-transformers>=2.2.0 + # Async asyncio>=3.4.3