feat(memory): fix(memory): rollover detector+watcher scan external project registries (DPLAN-0146)

Co-Authored-By: @memory <memory@aipass>
This commit is contained in:
AIOSAI
2026-04-22 23:01:21 -07:00
co-authored by @memory
parent 744ca3ab46
commit 4413df63ea
3 changed files with 86 additions and 28 deletions
@@ -46,6 +46,25 @@ def _find_repo_root() -> Path:
_REPO_ROOT = _find_repo_root()
def _find_caller_registries() -> List[Path]:
"""Find project registries reachable from CWD (for external projects)."""
import os
caller_cwd = (
Path(os.environ.get("AIPASS_CALLER_CWD", "")).resolve() if os.environ.get("AIPASS_CALLER_CWD") else Path.cwd()
)
aipass_registry = (_REPO_ROOT / "AIPASS_REGISTRY.json").resolve()
registries = []
for parent in [caller_cwd] + list(caller_cwd.parents):
for reg in parent.glob("*_REGISTRY.json"):
if reg.resolve() != aipass_registry:
registries.append(reg)
if registries:
break
return registries
# =============================================================================
# DATA STRUCTURES
# =============================================================================
@@ -74,17 +93,8 @@ class RolloverTrigger:
# =============================================================================
def _read_registry() -> List[Dict[str, Any]]:
"""
Read AIPASS_REGISTRY.json from repo root.
Registry paths are relative — resolved against repo root.
Returns:
List of branch dictionaries with absolute paths
"""
registry_path = _REPO_ROOT / "AIPASS_REGISTRY.json"
def _read_single_registry(registry_path: Path, root: Path) -> List[Dict[str, Any]]:
"""Read branches from a single registry file, resolving paths against root."""
if not registry_path.exists():
return []
@@ -93,20 +103,40 @@ def _read_registry() -> List[Dict[str, Any]]:
data = json.load(f)
branches = data.get("branches", [])
# Resolve relative paths against repo root
for branch in branches:
raw_path = branch.get("path", "")
resolved = Path(raw_path)
if not resolved.is_absolute():
resolved = _REPO_ROOT / raw_path
resolved = root / raw_path
branch["path"] = str(resolved)
return branches
except Exception as e:
logger.warning(f"[detector] Failed to read registry: {e}")
logger.warning(f"[detector] Failed to read registry {registry_path}: {e}")
return []
def _read_registry() -> List[Dict[str, Any]]:
"""
Read all project registries (AIPass + external projects from caller CWD).
Registry paths are relative — resolved against their respective project root.
Returns:
List of branch dictionaries with absolute paths
"""
branches = _read_single_registry(_REPO_ROOT / "AIPASS_REGISTRY.json", _REPO_ROOT)
seen_paths = {b.get("path") for b in branches}
for reg_path in _find_caller_registries():
for branch in _read_single_registry(reg_path, reg_path.parent):
if branch.get("path") not in seen_paths:
branches.append(branch)
seen_paths.add(branch.get("path"))
return branches
def _get_memory_file_path(branch: Dict, memory_type: str) -> Path | None:
"""
Get path to memory file for branch.
@@ -429,20 +429,10 @@ def _find_repo_root() -> Path:
return Path.cwd()
def _get_branch_paths() -> list[Path]:
"""
Get all branch paths from AIPASS_REGISTRY.json (silent - no logging).
Registry paths are relative — resolved against repo root.
Returns:
List of Path objects for each branch
"""
def _paths_from_registry(registry_path: Path, root: Path) -> list[Path]:
"""Read branch paths from a single registry file."""
import json
repo_root = _find_repo_root()
registry_path = repo_root / "AIPASS_REGISTRY.json"
if not registry_path.exists():
return []
@@ -456,16 +446,49 @@ def _get_branch_paths() -> list[Path]:
raw_path = branch.get("path", "")
branch_path = Path(raw_path)
if not branch_path.is_absolute():
branch_path = repo_root / raw_path
branch_path = root / raw_path
if branch_path.exists():
paths.append(branch_path)
return paths
except Exception as e:
logger.warning(f"[memory_watcher] Failed to read branch paths from registry: {e}")
logger.warning(f"[memory_watcher] Failed to read registry {registry_path}: {e}")
return []
def _get_branch_paths() -> list[Path]:
"""
Get all branch paths from AIPass registry + external project registries.
Returns:
List of Path objects for each branch
"""
import os
repo_root = _find_repo_root()
paths = _paths_from_registry(repo_root / "AIPASS_REGISTRY.json", repo_root)
seen = {p.resolve() for p in paths}
caller_cwd = (
Path(os.environ.get("AIPASS_CALLER_CWD", "")).resolve() if os.environ.get("AIPASS_CALLER_CWD") else Path.cwd()
)
aipass_registry = (repo_root / "AIPASS_REGISTRY.json").resolve()
found_external = False
for parent in [caller_cwd] + list(caller_cwd.parents):
for reg in parent.glob("*_REGISTRY.json"):
if reg.resolve() != aipass_registry:
found_external = True
for p in _paths_from_registry(reg, reg.parent):
if p.resolve() not in seen:
paths.append(p)
seen.add(p.resolve())
if found_external:
break
return paths
def _is_memory_file(file_path: Path) -> bool:
"""
Check if file is a memory file in .trinity/ (local.json or observations.json)
+5
View File
@@ -316,6 +316,7 @@ class TestReadRegistry:
# Point _REPO_ROOT at our tmp_path
monkeypatch.setattr(detector, "_REPO_ROOT", tmp_path)
monkeypatch.setattr(detector, "_find_caller_registries", lambda: [])
result = detector._read_registry()
@@ -329,6 +330,7 @@ class TestReadRegistry:
# Point _REPO_ROOT at a directory with no registry file
monkeypatch.setattr(detector, "_REPO_ROOT", tmp_path)
monkeypatch.setattr(detector, "_find_caller_registries", lambda: [])
result = detector._read_registry()
@@ -341,6 +343,7 @@ class TestReadRegistry:
from aipass.memory.apps.handlers.monitor import detector
monkeypatch.setattr(detector, "_REPO_ROOT", tmp_path)
monkeypatch.setattr(detector, "_find_caller_registries", lambda: [])
result = detector._read_registry()
@@ -358,6 +361,7 @@ class TestReadRegistry:
from aipass.memory.apps.handlers.monitor import detector
monkeypatch.setattr(detector, "_REPO_ROOT", tmp_path)
monkeypatch.setattr(detector, "_find_caller_registries", lambda: [])
result = detector._read_registry()
@@ -373,6 +377,7 @@ class TestReadRegistry:
from aipass.memory.apps.handlers.monitor import detector
monkeypatch.setattr(detector, "_REPO_ROOT", tmp_path)
monkeypatch.setattr(detector, "_find_caller_registries", lambda: [])
result = detector._read_registry()