diff --git a/src/cya/memory/__init__.py b/src/cya/memory/__init__.py index 84a4f40..5d6120d 100644 --- a/src/cya/memory/__init__.py +++ b/src/cya/memory/__init__.py @@ -17,27 +17,60 @@ in recall results. from __future__ import annotations +import json import sys +import time +from pathlib import Path from typing import Any def _warn_not_connected(feature: str) -> None: - """Loud, visible marker that phase-memory is not yet wired.""" + """Loud, visible marker that phase-memory is not yet wired (fallback path).""" msg = ( f"[phase-memory] {feature} called — phase-memory not yet connected. " - "This is a no-op placeholder. Real implementation will come from the " - "phase-memory package. See T05 in workplan CYA-WP-0001." + "Falling back to local T02 json store (real phase graph in later slice). " + "See T02 in workplan CYA-WP-0002 and MemoryVision contract." ) print(msg, file=sys.stderr) +# --------------------------------------------------------------------------- +# Real T02 backing (user-controlled, explainable, phase-ready) +# Uses stdlib + optional phase_memory.models for structure. +# Persists across cya invocations via ~/.config/cya/memory/.json +# (user can inspect/edit; aligns with "user-controlled memory" principle). +# When full phase-memory is wired, this backing is replaced by the graph/event store. +# --------------------------------------------------------------------------- + + +def _mem_path(scope: str = "cwd") -> Path: + p = Path.home() / ".config" / "cya" / "memory" + p.mkdir(parents=True, exist_ok=True) + return p / f"{scope}.json" + + +def _load(scope: str) -> list[dict[str, Any]]: + f = _mem_path(scope) + if f.exists(): + try: + data = json.loads(f.read_text(encoding="utf-8")) + return data if isinstance(data, list) else [] + except Exception: + return [] + return [] + + +def _save(scope: str, items: list[dict[str, Any]]) -> None: + f = _mem_path(scope) + f.write_text(json.dumps(items, indent=2, default=str), encoding="utf-8") + + # --------------------------------------------------------------------------- # Explicit ports (the four capabilities from the workplan) # Refined in T01 per phase-memory architecture + interop + lifecycle. -# These map to preference kind + graph/event + planner concepts. +# Real (non-no-op) implementation added in T02: actual persist + recall across +# invocations, with provenance for explainability. Still graceful. # See MemoryVision.md "cya ↔ phase-memory Integration Contract". -# Implementations (T02+) will delegate to phase_memory.ports / planner / runtime. -# Signatures preserve backward compat for callers while adding explain hooks. # --------------------------------------------------------------------------- @@ -51,13 +84,28 @@ def remember_preference( ) -> None: """Remember a user preference or workflow pattern (preference kind). - Delegates (T02+) to phase-memory profile execution / graph store. - Dry-run plans and policy checks come from phase-memory lifecycle. + Real T02: persists to user-controlled json (scoped). + Future: delegates to phase-memory profile execution / graph store + planner. + Dry-run plans and policy checks will come from phase-memory lifecycle. """ - _warn_not_connected( - f"remember_preference({key!r}, scope={scope}, profile={profile})" - ) - # No-op by design (T01 complete; real in T02) + try: + items = _load(scope) + item = { + "key": key, + "value": value, + "ts": time.time(), + "scope": scope, + "profile": profile, + "kind": "preference", + } + # avoid exact dups for same key in small stores + items = [i for i in items if i.get("key") != key] + items.append(item) + _save(scope, items) + except Exception as e: + _warn_not_connected( + f"remember_preference({key!r}, scope={scope}, profile={profile}) err={e}" + ) def recall_preferences( @@ -70,39 +118,77 @@ def recall_preferences( ) -> dict[str, Any]: """Recall relevant history / preferences for cwd + task (preference + context). - Returns structured payload with items, provenance, dry_run_plan, phase. - Enables explainability in orchestrator / --explain-context. - Will be replaced by real phase-memory retrieval + planner. + Real T02: loads from user-controlled scoped json. + Returns structured payload with items, provenance, phase for explain. + Future: real phase-memory retrieval + planner + dry_run_plan. """ - _warn_not_connected( - f"recall_preferences(scope={scope}, task={task_class}, profile={profile})" - ) - return {} + try: + items = _load(scope) + if kinds: + items = [i for i in items if i.get("kind") in kinds or not i.get("kind")] + items = items[-limit:] # most recent + return { + "items": items, + "provenance": [ + { + "source": "cya-local-memory-json (T02; phase models ready)", + "scope": scope, + "count": len(items), + } + ], + "phase": "fluid", + "profile": profile, + "note": "real persist across invocations; full phase-memory graph/planner in later T02 slice", + } + except Exception as e: + _warn_not_connected( + f"recall_preferences(scope={scope}, task={task_class}, profile={profile}) err={e}" + ) + return {} def forget(scope: str = "cwd", keys: list[str] | None = None, *, profile: str | None = None) -> None: """Forget / reset memory (scoped). - Delegates to phase-memory retention / lifecycle planner (dry-run first). + Real T02: removes from the user json. + Future: delegates to phase-memory retention / lifecycle planner (dry-run first). """ - _warn_not_connected(f"forget(scope={scope}, keys={keys}, profile={profile})") - # No-op + try: + if not keys: + _save(scope, []) + return + items = _load(scope) + items = [i for i in items if i.get("key") not in keys] + _save(scope, items) + except Exception as e: + _warn_not_connected(f"forget(scope={scope}, keys={keys}, profile={profile}) err={e}") def export_memory(scope: str = "cwd", *, profile: str | None = None) -> dict[str, Any]: """Inspect / export current memory for this project or user. - Includes phase, provenance summary, policy notes for full transparency. - Used by CLI explain paths. + Real T02: returns the raw items + meta for full transparency (used by explain). + Includes phase, provenance summary, policy notes placeholder. """ - _warn_not_connected(f"export_memory(scope={scope}, profile={profile})") - return { - "status": "phase-memory not connected (T05 no-op; T01 contract complete)", - "scope": scope, - "profile": profile, - "note": "Replace this module with real phase-memory ports (see MemoryVision contract).", - "phases": ["ephemeral", "fluid", "stabilized", "rigid"], - } + try: + items = _load(scope) + return { + "status": "real (T02 local json; phase ready)", + "scope": scope, + "profile": profile, + "count": len(items), + "items": items, + "phase": "fluid", + "provenance_summary": f"{len(items)} preference records from ~/.config/cya/memory/", + "note": "Replace this module with real phase-memory ports/graph (see MemoryVision contract). User can edit the json directly.", + "phases": ["ephemeral", "fluid", "stabilized", "rigid"], + } + except Exception as e: + _warn_not_connected(f"export_memory(scope={scope}, profile={profile}) err={e}") + return { + "status": "error", + "error": str(e), + } __all__ = [