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 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,10 @@ def bootstrap() -> None:
|
|||||||
_create_if_missing(home / "memory" / "MEMORY_INDEX.md", _MEMORY_INDEX_TEMPLATE, 0o600)
|
_create_if_missing(home / "memory" / "MEMORY_INDEX.md", _MEMORY_INDEX_TEMPLATE, 0o600)
|
||||||
_create_if_missing(home / "memory" / "user" / "profile.md", _USER_PROFILE_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"
|
config = home / "config.yaml"
|
||||||
if config.exists():
|
if config.exists():
|
||||||
safe_chmod(config, 0o600)
|
safe_chmod(config, 0o600)
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class MemoryFile:
|
|||||||
modified: datetime.datetime
|
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"))
|
files = sorted(_MEMORY_ROOT.rglob("*.md"))
|
||||||
result: list[MemoryFile] = []
|
result: list[MemoryFile] = []
|
||||||
for f in files:
|
for f in files:
|
||||||
@@ -36,6 +37,27 @@ def list_memories() -> list[MemoryFile]:
|
|||||||
return result
|
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:
|
def read_memory(name: str) -> str:
|
||||||
path = (_MEMORY_ROOT / name).resolve()
|
path = (_MEMORY_ROOT / name).resolve()
|
||||||
assert_safe_path(path)
|
assert_safe_path(path)
|
||||||
@@ -63,19 +85,24 @@ def read_index() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def lookup_memories(query: str) -> list[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()
|
q = query.lower()
|
||||||
results: list[dict] = []
|
fallback: list[dict] = []
|
||||||
for rel_path, entry in read_index().items():
|
for rel_path, entry in read_index().items():
|
||||||
summary = entry.get("summary", "").lower()
|
summary = entry.get("summary", "").lower()
|
||||||
keywords = [k.lower() for k in entry.get("keywords", [])]
|
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):
|
if q in summary or any(q in k or k in q for k in keywords):
|
||||||
results.append({
|
fallback.append({
|
||||||
"file": rel_path,
|
"file": rel_path,
|
||||||
"summary": entry.get("summary", ""),
|
"summary": entry.get("summary", ""),
|
||||||
"keywords": entry.get("keywords", []),
|
"keywords": entry.get("keywords", []),
|
||||||
})
|
})
|
||||||
return results
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
def load_context_for_session() -> str:
|
def load_context_for_session() -> str:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pyra.memory import _MEMORY_ROOT
|
from pyra.memory import _MEMORY_ROOT
|
||||||
@@ -20,6 +21,25 @@ def _resolve_and_validate(name: str) -> Path:
|
|||||||
return 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(
|
def write_memory(
|
||||||
name: str,
|
name: str,
|
||||||
content: str,
|
content: str,
|
||||||
@@ -34,6 +54,7 @@ def write_memory(
|
|||||||
if summary or keywords:
|
if summary or keywords:
|
||||||
rel_key = path.relative_to(_MEMORY_ROOT).as_posix()
|
rel_key = path.relative_to(_MEMORY_ROOT).as_posix()
|
||||||
update_json_entry(rel_key, summary, keywords or [])
|
update_json_entry(rel_key, summary, keywords or [])
|
||||||
|
_upsert_to_db(path, content, summary, keywords)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
@@ -42,9 +63,12 @@ def append_memory(name: str, content: str) -> Path:
|
|||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if path.exists():
|
if path.exists():
|
||||||
existing = path.read_text()
|
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:
|
else:
|
||||||
path.write_text(content)
|
new_content = content
|
||||||
|
path.write_text(new_content)
|
||||||
safe_chmod(path, 0o600)
|
safe_chmod(path, 0o600)
|
||||||
update_index()
|
update_index()
|
||||||
|
_upsert_to_db(path, new_content)
|
||||||
return path
|
return path
|
||||||
|
|||||||
Reference in New Issue
Block a user