| | """Consent-based memory with local-first persistence.""" |
| |
|
| | from __future__ import annotations |
| |
|
| | import json |
| | from dataclasses import dataclass, asdict |
| | from datetime import datetime, timezone |
| | from pathlib import Path |
| | from typing import Dict, List, Optional |
| |
|
| | from .perception import PerceptionInput |
| |
|
| |
|
| | @dataclass |
| | class MemoryEntry: |
| | fingerprint: str |
| | consent: bool |
| | summary: str |
| | tags: List[str] |
| | timestamp: str |
| | metadata: Dict[str, str] |
| |
|
| | @classmethod |
| | def from_dict(cls, payload: Dict[str, str]) -> "MemoryEntry": |
| | return cls( |
| | fingerprint=payload["fingerprint"], |
| | consent=payload["consent"], |
| | summary=payload["summary"], |
| | tags=list(payload.get("tags", [])), |
| | timestamp=payload["timestamp"], |
| | metadata=dict(payload.get("metadata", {})), |
| | ) |
| |
|
| |
|
| | class ConsentMemory: |
| | """Stores entries locally only when consent is provided.""" |
| |
|
| | def __init__(self, path: Path | None = None) -> None: |
| | self._path = path or Path.home() / ".config" / "blux-ca" / "memory" / "consent.jsonl" |
| | self._path.parent.mkdir(parents=True, exist_ok=True) |
| | self._session: Dict[str, MemoryEntry] = {} |
| |
|
| | def store( |
| | self, |
| | perception: PerceptionInput, |
| | *, |
| | consent: bool, |
| | summary: str, |
| | metadata: Optional[Dict[str, str]] = None, |
| | ) -> MemoryEntry: |
| | entry = MemoryEntry( |
| | fingerprint=perception.fingerprint, |
| | consent=consent, |
| | summary=summary, |
| | tags=list(perception.tags), |
| | timestamp=datetime.now(timezone.utc).isoformat(), |
| | metadata=dict(metadata or {}), |
| | ) |
| | self._session[entry.fingerprint] = entry |
| | if consent: |
| | self._append(entry) |
| | return entry |
| |
|
| | def _append(self, entry: MemoryEntry) -> None: |
| | with self._path.open("a", encoding="utf-8") as handle: |
| | handle.write(json.dumps(asdict(entry), ensure_ascii=False) + "\n") |
| |
|
| | def session_entries(self) -> List[MemoryEntry]: |
| | return list(self._session.values()) |
| |
|
| | def persistent_entries(self, *, limit: int | None = None) -> List[MemoryEntry]: |
| | if not self._path.exists(): |
| | return [] |
| | entries: List[MemoryEntry] = [] |
| | with self._path.open("r", encoding="utf-8") as handle: |
| | for line in handle: |
| | if not line.strip(): |
| | continue |
| | entries.append(MemoryEntry.from_dict(json.loads(line))) |
| | if limit and len(entries) >= limit: |
| | break |
| | return entries |
| |
|
| |
|
| | __all__ = ["ConsentMemory", "MemoryEntry"] |
| |
|