From b9b0918d3a2d6f73b2718a78f891fa2c36d781d6 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 18 May 2026 15:23:49 +0200 Subject: [PATCH] feat(memory): wire database into reader, writer, and bootstrap - reader: list_memories() queries memory_meta; lookup_memories() uses FTS5 with fallback to JSON index substring search - writer: write_memory() and append_memory() upsert to DB after every file write - dirs: bootstrap() calls init_db() + migrate_from_files() on startup Existing .md files remain the canonical store; SQLite is the search index. Co-Authored-By: Claude Sonnet 4.6 --- src/pyra/config/dirs.py | 4 ++++ src/pyra/memory/reader.py | 37 ++++++++++++++++++++++++++++++++----- src/pyra/memory/writer.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/pyra/config/dirs.py b/src/pyra/config/dirs.py index 5cc25e1..74d1db8 100644 --- a/src/pyra/config/dirs.py +++ b/src/pyra/config/dirs.py @@ -43,6 +43,10 @@ def bootstrap() -> None: _create_if_missing(home / "memory" / "MEMORY_INDEX.md", _MEMORY_INDEX_TEMPLATE, 0o600) _create_if_missing(home / "memory" / "user" / "profile.md", _USER_PROFILE_TEMPLATE, 0o600) + from pyra.memory.database import init_db, migrate_from_files + init_db() + migrate_from_files() + config = home / "config.yaml" if config.exists(): safe_chmod(config, 0o600) diff --git a/src/pyra/memory/reader.py b/src/pyra/memory/reader.py index 8afe501..da9f32d 100644 --- a/src/pyra/memory/reader.py +++ b/src/pyra/memory/reader.py @@ -18,7 +18,8 @@ class MemoryFile: modified: datetime.datetime -def list_memories() -> list[MemoryFile]: +def _scan_files() -> list[MemoryFile]: + """Fallback: scan .md files directly (used when DB is unavailable).""" files = sorted(_MEMORY_ROOT.rglob("*.md")) result: list[MemoryFile] = [] for f in files: @@ -36,6 +37,27 @@ def list_memories() -> list[MemoryFile]: return result +def list_memories() -> list[MemoryFile]: + from pyra.memory import database + rows = database.list_all() + if not rows: + return _scan_files() + result: list[MemoryFile] = [] + for row in rows: + try: + modified = datetime.datetime.fromisoformat(row["modified"]) + except (ValueError, TypeError): + modified = datetime.datetime.now() + result.append(MemoryFile( + name=row["path"], + path=_MEMORY_ROOT / row["path"], + category=row["category"], + size_bytes=row["size_bytes"], + modified=modified, + )) + return result + + def read_memory(name: str) -> str: path = (_MEMORY_ROOT / name).resolve() assert_safe_path(path) @@ -63,19 +85,24 @@ def read_index() -> dict: def lookup_memories(query: str) -> list[dict]: - """Case-insensitive substring search over summary text and keywords.""" + """Full-text search via FTS5; falls back to JSON index substring search.""" + from pyra.memory import database + results = database.search(query) + if results: + return results + # Fallback: case-insensitive substring search over JSON index q = query.lower() - results: list[dict] = [] + fallback: list[dict] = [] for rel_path, entry in read_index().items(): summary = entry.get("summary", "").lower() keywords = [k.lower() for k in entry.get("keywords", [])] if q in summary or any(q in k or k in q for k in keywords): - results.append({ + fallback.append({ "file": rel_path, "summary": entry.get("summary", ""), "keywords": entry.get("keywords", []), }) - return results + return fallback def load_context_for_session() -> str: diff --git a/src/pyra/memory/writer.py b/src/pyra/memory/writer.py index 168c43c..40774cf 100644 --- a/src/pyra/memory/writer.py +++ b/src/pyra/memory/writer.py @@ -1,3 +1,4 @@ +import datetime from pathlib import Path from pyra.memory import _MEMORY_ROOT @@ -20,6 +21,25 @@ def _resolve_and_validate(name: str) -> Path: return path +def _upsert_to_db(path: Path, content: str, summary: str = "", keywords: list[str] | None = None) -> None: + from pyra.memory import database + if not database._DB_PATH.exists(): + return + rel = path.relative_to(_MEMORY_ROOT).as_posix() + category = rel.split("/")[0] if "/" in rel else "root" + stat = path.stat() + mtime = datetime.datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds") + database.upsert( + rel, + content=content, + category=category, + size_bytes=stat.st_size, + modified=mtime, + summary=summary, + keywords=keywords, + ) + + def write_memory( name: str, content: str, @@ -34,6 +54,7 @@ def write_memory( if summary or keywords: rel_key = path.relative_to(_MEMORY_ROOT).as_posix() update_json_entry(rel_key, summary, keywords or []) + _upsert_to_db(path, content, summary, keywords) return path @@ -42,9 +63,12 @@ def append_memory(name: str, content: str) -> Path: path.parent.mkdir(parents=True, exist_ok=True) if path.exists(): existing = path.read_text() - path.write_text(existing.rstrip() + "\n\n" + content) + new_content = existing.rstrip() + "\n\n" + content + path.write_text(new_content) else: - path.write_text(content) + new_content = content + path.write_text(new_content) safe_chmod(path, 0o600) update_index() + _upsert_to_db(path, new_content) return path