feat(memory): fix(memory): rollover detector+watcher scan external project registries (DPLAN-0146)
Co-Authored-By: @memory <memory@aipass>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user