- memory.config.json is the single source of truth: char caps (entry_limits, global) + rollover counts (per_branch, materialized from registry); .trinity files stripped to a one-line _usage header - changed_entries gate now matches by content identity, not array position — prepending a new entry never re-flags unchanged legacy entries (old=old, new=new; no trimming required on a cap change) - rollover push command + top-level 'drone @memory push' alias; corrected config _note + --help (rollover push surfaced, labeled destructive system-wide reset) - removed 3 dead functions: seed_per_branch, its orphaned write_config, add_learning + its tests - spawn birthright/builder + LOCAL/OBSERVATIONS templates aligned to the stripped shape - 966 tests, seedgo 100% Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
550 lines
18 KiB
Python
Executable File
550 lines
18 KiB
Python
Executable File
# =================== AIPass ====================
|
|
# Name: rollover.py
|
|
# Description: Rollover Orchestration Module
|
|
# Version: 0.6.0
|
|
# Created: 2025-11-16
|
|
# Modified: 2026-03-15
|
|
# =============================================
|
|
|
|
"""
|
|
Rollover Orchestration Module
|
|
|
|
Coordinates the memory rollover workflow by calling handlers in sequence:
|
|
1. Detect rollover triggers (monitor/detector)
|
|
2. Extract oldest memories (rollover/extractor)
|
|
3. Generate embeddings (vector/embedder)
|
|
4. Store in Chroma (storage/chroma)
|
|
|
|
Purpose:
|
|
Thin orchestration layer - no business logic implementation.
|
|
All domain logic lives in handlers.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from rich.panel import Panel
|
|
from rich import box
|
|
|
|
from aipass.prax import logger
|
|
from aipass.cli.apps.modules import console, error, warning
|
|
from aipass.memory.apps.handlers.json import json_handler
|
|
|
|
# =============================================================================
|
|
# INFRASTRUCTURE SETUP
|
|
# =============================================================================
|
|
|
|
# Handler imports
|
|
from ..handlers.monitor import detector
|
|
from ..handlers.rollover.orchestrator import (
|
|
execute_rollover as _handler_execute_rollover,
|
|
sync_line_counts as _handler_sync_line_counts,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# COMMAND HANDLERS
|
|
# =============================================================================
|
|
|
|
_SUBCOMMANDS = {
|
|
"run": "Execute rollover for files exceeding limits",
|
|
"status": "Show rollover statistics for all branches",
|
|
"check": "Check which files need rollover (dry run)",
|
|
"sync-lines": "Update line count metadata for all branches",
|
|
"push": "Overwrite all per_branch limits to defaults (system-wide reset)",
|
|
}
|
|
|
|
|
|
def handle_command(command: str, args: List[str]) -> bool:
|
|
"""
|
|
Handle rollover commands with seedgo-compliant introspection.
|
|
|
|
Routing:
|
|
rollover (no args) -> print_introspection()
|
|
rollover --help/-h/help -> print_help()
|
|
rollover run -> execute rollover
|
|
rollover status -> show rollover status
|
|
rollover check -> dry-run check
|
|
rollover sync-lines -> sync line counts
|
|
|
|
Backward-compatible top-level commands (routed from entry point):
|
|
status, check, sync-lines -> forwarded directly
|
|
|
|
Args:
|
|
command: Command name
|
|
args: Additional arguments
|
|
|
|
Returns:
|
|
True if command handled, False otherwise
|
|
"""
|
|
# Top-level help (backward compat — entry point may send these)
|
|
if command in ("--help", "-h", "help"):
|
|
print_help()
|
|
return True
|
|
|
|
if command == "rollover":
|
|
# No args → introspection (seedgo standard)
|
|
if not args:
|
|
print_introspection()
|
|
return True
|
|
|
|
# --help / -h / help → full help
|
|
if args[0] in ("--help", "-h", "help"):
|
|
print_help()
|
|
return True
|
|
|
|
# Subcommand routing
|
|
sub = args[0]
|
|
|
|
if sub == "run":
|
|
run_rollover()
|
|
return True
|
|
|
|
if sub == "status":
|
|
show_status()
|
|
return True
|
|
|
|
if sub == "check":
|
|
check_triggers()
|
|
return True
|
|
|
|
if sub == "sync-lines":
|
|
sync_line_counts()
|
|
return True
|
|
|
|
if sub == "push":
|
|
push_defaults()
|
|
return True
|
|
|
|
# Unknown subcommand
|
|
error(
|
|
f"Unknown subcommand: '{sub}'",
|
|
suggestion="Available: " + ", ".join(_SUBCOMMANDS.keys()),
|
|
)
|
|
return True
|
|
|
|
# Backward-compatible top-level commands (entry point still routes these)
|
|
if command == "status":
|
|
show_status()
|
|
return True
|
|
|
|
elif command == "check":
|
|
check_triggers()
|
|
return True
|
|
|
|
elif command == "sync-lines":
|
|
sync_line_counts()
|
|
return True
|
|
|
|
elif command == "process-plans":
|
|
process_plans_command()
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def print_help() -> None:
|
|
"""Display rollover module help"""
|
|
console.print()
|
|
console.print(
|
|
Panel.fit(
|
|
"[bold cyan]Rollover Module - Memory Rollover Orchestration[/bold cyan]",
|
|
border_style="cyan",
|
|
box=box.ROUNDED,
|
|
)
|
|
)
|
|
console.print()
|
|
console.print("[bold]USAGE:[/bold]")
|
|
console.print(" drone @memory rollover <command>")
|
|
console.print()
|
|
console.print("[bold]COMMANDS:[/bold]")
|
|
console.print(" [cyan]rollover[/cyan] Execute rollover for files exceeding limits")
|
|
console.print(" [cyan]status[/cyan] Show rollover statistics for all branches")
|
|
console.print(" [cyan]check[/cyan] Check which files need rollover (dry run)")
|
|
console.print(" [cyan]sync-lines[/cyan] Update line count metadata for all branches")
|
|
console.print(" [cyan]push[/cyan] ⚠ Reset ALL per_branch limits to defaults (system-wide)")
|
|
console.print(" [cyan]help[/cyan] Show this help message")
|
|
console.print()
|
|
console.print("[bold]LIMITS:[/bold]")
|
|
console.print(" v1 (schema <2.0): Line-count based (max_lines, default 600)")
|
|
console.print(" v2 (schema 2.0+): Entry-count based (max_sessions, max_key_learnings)")
|
|
console.print()
|
|
console.print("[bold]WORKFLOW:[/bold]")
|
|
console.print(" 1. Detect files exceeding limits (line count or entry count)")
|
|
console.print(" 2. Extract oldest entries")
|
|
console.print(" 3. Generate embeddings via fastembed")
|
|
console.print(" 4. Store vectors in local + global ChromaDB")
|
|
console.print()
|
|
|
|
|
|
# =============================================================================
|
|
# ROLLOVER ORCHESTRATION
|
|
# =============================================================================
|
|
|
|
|
|
def run_rollover() -> bool:
|
|
"""
|
|
Execute rollover workflow for all triggered branches.
|
|
|
|
Delegates to handler and renders results with Rich.
|
|
"""
|
|
console.print()
|
|
console.print(Panel.fit("[bold cyan]Memory - Rollover Execution[/bold cyan]", border_style="cyan", box=box.ROUNDED))
|
|
console.print()
|
|
|
|
console.print("[cyan]Checking for rollover triggers... (first run may take 30s for model loading)[/cyan]")
|
|
|
|
try:
|
|
result = _handler_execute_rollover()
|
|
except Exception as e:
|
|
logger.error(f"[rollover] Rollover execution failed: {e}", exc_info=True)
|
|
error(f"Rollover failed: {e}")
|
|
return False
|
|
|
|
if not result.get("success") and result.get("error"):
|
|
error(result["error"])
|
|
return False
|
|
|
|
triggers_count = result.get("triggers_count", 0)
|
|
if triggers_count == 0:
|
|
console.print("[green]>[/green] No files need rollover")
|
|
return True
|
|
|
|
console.print(f"[green]>[/green] Found {triggers_count} files ready for rollover")
|
|
console.print()
|
|
|
|
# Display individual results
|
|
for item in result.get("results", []):
|
|
local_status = "> local" if item.get("local_stored") else "x local"
|
|
console.print(
|
|
f" [green]>[/green] Rolled over {item['memories_count']} items -> {item['global_collection']} "
|
|
f"({item['old_lines']} -> {item['new_lines']} lines, "
|
|
f"global: {item['global_total']} vectors, {local_status})"
|
|
)
|
|
|
|
# Report results
|
|
success_count = result.get("success_count", 0)
|
|
failed = result.get("failed", [])
|
|
|
|
console.print()
|
|
if success_count > 0:
|
|
console.print(f"[green]>[/green] Rollover complete: {success_count}/{triggers_count} successful")
|
|
|
|
if failed:
|
|
console.print()
|
|
for fail in failed:
|
|
error(f"{fail['trigger']} - {fail['stage']}: {fail['error']}")
|
|
|
|
json_handler.log_operation("rollover_execute", {"triggers": triggers_count, "success_count": success_count})
|
|
return success_count > 0
|
|
|
|
|
|
# =============================================================================
|
|
# PLAN VECTORIZATION
|
|
# =============================================================================
|
|
|
|
|
|
def process_plans_command() -> None:
|
|
"""
|
|
Process pending plan files into vector storage.
|
|
|
|
Batches all chunks from all files into a single embed + store call.
|
|
"""
|
|
console.print()
|
|
console.print(Panel.fit("[bold cyan]Memory - Process Plans[/bold cyan]", border_style="cyan", box=box.ROUNDED))
|
|
console.print()
|
|
|
|
console.print("[cyan]Processing plan files into vector storage...[/cyan]")
|
|
console.print()
|
|
|
|
try:
|
|
from ..handlers.intake.plans_processor import process_plans
|
|
|
|
result = process_plans()
|
|
except Exception as e:
|
|
logger.error(f"[rollover] Plan processing failed: {e}")
|
|
error(f"Plan processing failed: {e}")
|
|
return
|
|
|
|
if not result.get("success"):
|
|
error(result.get("error", "Unknown error"))
|
|
if result.get("errors"):
|
|
for err in result["errors"]:
|
|
error(err)
|
|
return
|
|
|
|
files_processed = result.get("files_processed", 0)
|
|
total_chunks = result.get("total_chunks", 0)
|
|
reason = result.get("reason", "")
|
|
|
|
if files_processed == 0 and reason:
|
|
console.print(f"[green]>[/green] {reason}")
|
|
elif files_processed == 0:
|
|
console.print("[green]>[/green] No new plans to process")
|
|
else:
|
|
console.print(f"[green]>[/green] Processed {files_processed} files ({total_chunks} chunks vectorized)")
|
|
|
|
if result.get("errors"):
|
|
console.print()
|
|
for err in result["errors"]:
|
|
error(err)
|
|
|
|
console.print()
|
|
json_handler.log_operation(
|
|
"process_plans_command", {"files_processed": files_processed, "total_chunks": total_chunks}
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# LINE COUNT SYNC
|
|
# =============================================================================
|
|
|
|
|
|
def sync_line_counts() -> None:
|
|
"""
|
|
Update line count metadata for all branch memory files.
|
|
|
|
Delegates to handler and renders results with Rich.
|
|
"""
|
|
console.print()
|
|
console.print(Panel.fit("[bold cyan]Memory - Sync Line Counts[/bold cyan]", border_style="cyan", box=box.ROUNDED))
|
|
console.print()
|
|
|
|
console.print("[cyan]Updating line counts for all memory files...[/cyan]")
|
|
console.print()
|
|
|
|
result = _handler_sync_line_counts()
|
|
|
|
if result["success"]:
|
|
console.print(f"[green]>[/green] Updated {result['updated']} files")
|
|
if result["failed"] > 0:
|
|
warning(f"{result['failed']} files failed")
|
|
for branch, mem_type, err_msg in result.get("failures", []):
|
|
error(f"{branch}.{mem_type}: {err_msg}")
|
|
json_handler.log_operation("rollover_sync_lines", {"updated": result["updated"], "failed": result["failed"]})
|
|
else:
|
|
error("Failed to sync line counts")
|
|
|
|
console.print()
|
|
|
|
|
|
# =============================================================================
|
|
# PUSH DEFAULTS
|
|
# =============================================================================
|
|
|
|
|
|
def push_defaults() -> None:
|
|
"""Overwrite every per_branch entry in memory.config.json with defaults."""
|
|
from ..handlers.json import config_loader
|
|
|
|
console.print()
|
|
console.print(Panel.fit("[bold cyan]Memory - Push Defaults[/bold cyan]", border_style="cyan", box=box.ROUNDED))
|
|
console.print()
|
|
|
|
console.print("[cyan]Overwriting all per_branch limits with defaults...[/cyan]")
|
|
console.print()
|
|
|
|
result = config_loader.push_defaults_to_per_branch()
|
|
|
|
if not result.get("success"):
|
|
error(result.get("error", "Unknown error"))
|
|
return
|
|
|
|
count = result.get("branches", 0)
|
|
console.print(f"[green]>[/green] Pushed defaults to {count} branches")
|
|
console.print()
|
|
json_handler.log_operation("push_defaults", {"branches": count})
|
|
|
|
|
|
# =============================================================================
|
|
# STATUS & CHECKING
|
|
# =============================================================================
|
|
|
|
|
|
def show_status() -> None:
|
|
"""
|
|
Show rollover statistics for all branches
|
|
|
|
Displays:
|
|
- Files checked
|
|
- Files ready for rollover
|
|
- Per-branch status (current/max lines)
|
|
"""
|
|
console.print()
|
|
console.print(Panel.fit("[bold cyan]Memory - Rollover Status[/bold cyan]", border_style="cyan", box=box.ROUNDED))
|
|
console.print()
|
|
|
|
# Get stats from detector
|
|
stats_result = detector.get_rollover_stats()
|
|
|
|
if not stats_result["success"]:
|
|
error(f"Failed to get status: {stats_result.get('error', 'Unknown error')}")
|
|
logger.error(f"[rollover] Failed to get status: {stats_result.get('error')}")
|
|
return
|
|
|
|
stats = stats_result
|
|
|
|
# Summary
|
|
console.print(f"[cyan]Branches:[/cyan] {stats['total_branches']}")
|
|
console.print(f"[cyan]Files checked:[/cyan] {stats['files_checked']}")
|
|
console.print(f"[cyan]Ready for rollover:[/cyan] {stats['files_ready']}")
|
|
console.print()
|
|
|
|
# Per-branch details
|
|
if stats["branches"]:
|
|
console.print("[bold cyan]Branch Details:[/bold cyan]")
|
|
console.print()
|
|
|
|
for branch_name, branch_stats in stats["branches"].items():
|
|
console.print(f" [bold]{branch_name}[/bold]")
|
|
|
|
for memory_type, file_stats in branch_stats.items():
|
|
current = file_stats["current"]
|
|
max_val = file_stats["max"]
|
|
ready = file_stats["ready"]
|
|
remaining = file_stats["remaining"]
|
|
schema_ver = file_stats.get("schema_version", "1.0.0")
|
|
v2_reason = file_stats.get("v2_reason", "")
|
|
|
|
status_marker = "[red]![/red]" if ready else "[green]OK[/green]"
|
|
|
|
if schema_ver.startswith("2"):
|
|
status_text = f"READY ({v2_reason})" if ready else "OK (v2)"
|
|
console.print(f" {status_marker} {memory_type}: {status_text}")
|
|
else:
|
|
status_text = "READY" if ready else f"{remaining} remaining"
|
|
console.print(f" {status_marker} {memory_type}: {current}/{max_val} lines ({status_text})")
|
|
|
|
console.print()
|
|
|
|
json_handler.log_operation(
|
|
"rollover_status", {"branches_checked": stats["total_branches"], "files_ready": stats["files_ready"]}
|
|
)
|
|
|
|
|
|
def check_triggers() -> None:
|
|
"""
|
|
Check which branches need rollover (without executing)
|
|
|
|
Displays list of files that hit rollover threshold
|
|
"""
|
|
console.print()
|
|
console.print(Panel.fit("[bold cyan]Memory - Rollover Check[/bold cyan]", border_style="cyan", box=box.ROUNDED))
|
|
console.print()
|
|
|
|
triggers_result = detector.check_all_branches()
|
|
|
|
if not triggers_result["success"]:
|
|
error(f"Failed to check triggers: {triggers_result.get('error', 'Unknown error')}")
|
|
logger.error(f"[rollover] Failed to check triggers: {triggers_result.get('error')}")
|
|
return
|
|
|
|
triggers = triggers_result.get("triggers", [])
|
|
|
|
if not triggers:
|
|
console.print("[green]>[/green] No files need rollover")
|
|
json_handler.log_operation("rollover_check", {"files_needing_rollover": 0})
|
|
return
|
|
|
|
console.print(f"[bold cyan]Found {len(triggers)} files ready for rollover:[/bold cyan]")
|
|
console.print()
|
|
|
|
for trigger in triggers:
|
|
console.print(f" * {trigger}")
|
|
|
|
console.print()
|
|
console.print("[dim]Run 'drone @memory rollover' to process these files[/dim]")
|
|
console.print()
|
|
json_handler.log_operation("rollover_check", {"files_needing_rollover": len(triggers)})
|
|
|
|
|
|
# =============================================================================
|
|
# INTROSPECTION
|
|
# =============================================================================
|
|
|
|
|
|
def _discover_handlers() -> dict[str, list[str]]:
|
|
"""Auto-discover handler directories and their Python files.
|
|
|
|
Scans the handlers/ directory relative to this module.
|
|
|
|
Returns:
|
|
Dict mapping handler directory name to list of .py filenames
|
|
(excluding __init__.py and __pycache__).
|
|
"""
|
|
handlers_dir = Path(__file__).resolve().parent.parent / "handlers"
|
|
result: dict[str, list[str]] = {}
|
|
if not handlers_dir.exists():
|
|
return result
|
|
for d in sorted(handlers_dir.iterdir()):
|
|
if not d.is_dir() or d.name.startswith("__"):
|
|
continue
|
|
py_files = sorted(f.name for f in d.iterdir() if f.is_file() and f.suffix == ".py" and f.name != "__init__.py")
|
|
if py_files:
|
|
result[d.name] = py_files
|
|
return result
|
|
|
|
|
|
def print_introspection() -> None:
|
|
"""Display module introspection info (seedgo standard).
|
|
|
|
Called when 'rollover' is invoked with no arguments.
|
|
Shows module identity, connected handlers, available subcommands,
|
|
and next-step hints.
|
|
"""
|
|
console.print()
|
|
console.print("[bold cyan]rollover Module[/bold cyan]")
|
|
console.print("Orchestrates memory rollover workflow: trigger detection, extraction, embedding, and vector storage")
|
|
console.print()
|
|
|
|
# Connected handlers (auto-discovered)
|
|
handlers = _discover_handlers()
|
|
console.print("[yellow]Connected Handlers:[/yellow]")
|
|
if handlers:
|
|
for dir_name, files in handlers.items():
|
|
file_list = ", ".join(files)
|
|
console.print(f" [cyan]handlers/{dir_name}/[/cyan] [dim]{file_list}[/dim]")
|
|
else:
|
|
console.print(" [dim]No handlers found[/dim]")
|
|
console.print()
|
|
|
|
# Available subcommands
|
|
console.print("[yellow]Subcommands:[/yellow]")
|
|
for sub, desc in _SUBCOMMANDS.items():
|
|
console.print(f" [green]{sub:<14}[/green] {desc}")
|
|
console.print()
|
|
|
|
# Next-step hints
|
|
console.print("[yellow]Next:[/yellow]")
|
|
console.print(" [green]drone @memory rollover run[/green] [dim]# Execute rollover[/dim]")
|
|
console.print(" [green]drone @memory rollover status[/green] [dim]# View rollover stats[/dim]")
|
|
console.print(" [green]drone @memory rollover check[/green] [dim]# Dry-run check[/dim]")
|
|
console.print(" [green]drone @memory rollover --help[/green] [dim]# Full usage guide[/dim]")
|
|
console.print()
|
|
|
|
|
|
# =============================================================================
|
|
# STANDALONE EXECUTION
|
|
# =============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
# No args → introspection (seedgo standard)
|
|
if len(sys.argv) < 2:
|
|
handle_command("rollover", [])
|
|
sys.exit(0)
|
|
|
|
# --help → full help
|
|
if sys.argv[1] in ("--help", "-h", "help"):
|
|
handle_command("rollover", ["--help"])
|
|
sys.exit(0)
|
|
|
|
# Execute command via handle_command
|
|
command = sys.argv[1]
|
|
if not handle_command(command, sys.argv[2:]):
|
|
console.print(f"[red]Unknown command:[/red] {command}")
|
|
console.print("Run with [cyan]help[/cyan] for available commands")
|
|
sys.exit(1)
|