Files
AIPass/src/aipass/memory/apps/modules/rollover.py
T
AIOSAIandClaude Opus 4.6 97952124b5 feat(system): stderr routing migration Phase 3 — 10 branches migrated (FPLAN-0033)
Migrated error()/warning() calls across 10 branches to use CLI display API
which routes to stderr. 48 files modified across ai_mail, api, backup, daemon,
flow, memory, prax, seedgo, spawn, trigger. Seedgo stderr_routing standard
created (24th standard) with full-file scanning. System average 49% → 69%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 00:41:38 -07:00

355 lines
12 KiB
Python
Executable File

# =================== AIPass ====================
# Name: rollover.py
# Description: Rollover Orchestration Module
# Version: 0.5.0
# Created: 2025-11-16
# Modified: 2026-03-08
# =============================================
"""
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 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
# =============================================================================
# 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
# =============================================================================
def handle_command(command: str, args: List[str]) -> bool: # noqa: ARG001
"""
Handle rollover commands
Commands supported:
- rollover: Execute rollover for triggered branches
- status: Show rollover statistics
- check: Check which branches need rollover
- sync-lines: Update line count metadata for all branches
Args:
command: Command name
args: Additional arguments
Returns:
True if command handled, False otherwise
"""
if command in ('--help', '-h', 'help'):
print_help()
return True
if command == 'rollover':
run_rollover()
return True
elif command == 'status':
show_status()
return True
elif command == 'check':
check_triggers()
return True
elif command == 'sync-lines':
sync_line_counts()
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(" python3 -m aipass.memory.apps.modules.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]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 sentence-transformers")
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...[/cyan]")
result = _handler_execute_rollover()
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, 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']}")
return success_count > 0
# =============================================================================
# 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}")
else:
error("Failed to sync line counts")
console.print()
# =============================================================================
# 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()
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")
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()
# =============================================================================
# INTROSPECTION
# =============================================================================
def print_introspection():
"""Display module introspection info."""
console.print()
console.print("rollover Module")
console.print("Orchestrates memory rollover workflow: trigger detection, extraction, embedding, and vector storage")
console.print()
console.print("Connected Handlers:")
console.print(" handlers/monitor/")
console.print(" - detector.py (check_all_branches — detect branches exceeding rollover threshold)")
console.print(" - detector.py (get_rollover_stats — retrieve rollover statistics for all branches)")
console.print(" handlers/rollover/")
console.print(" - orchestrator.py (execute_rollover — run full rollover pipeline for triggered branches)")
console.print(" - orchestrator.py (sync_line_counts — update line count metadata for all memory files)")
console.print()
# =============================================================================
# STANDALONE EXECUTION
# =============================================================================
if __name__ == "__main__":
import sys
# Handle --help before argparse (module standard)
if len(sys.argv) < 2 or sys.argv[1] in ('--help', '-h', 'help'):
handle_command('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)