Fix project isolation: Make loadChatHistory respect active project sessions

- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-22 14:43:05 +00:00
Unverified
parent b82837aa5f
commit 55aafbae9a
6463 changed files with 1115462 additions and 4486 deletions

View File

@@ -0,0 +1,626 @@
#!/usr/bin/env python3
# ABOUTME: Ralph orchestrator main loop implementation with multi-agent support
# ABOUTME: Implements the core Ralph Wiggum technique with continuous iteration
import sys
import logging
import argparse
import threading
import yaml
from pathlib import Path
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, field
from enum import Enum
from .orchestrator import RalphOrchestrator
# Configuration defaults
DEFAULT_MAX_ITERATIONS = 100
DEFAULT_MAX_RUNTIME = 14400 # 4 hours
DEFAULT_PROMPT_FILE = "PROMPT.md"
DEFAULT_CHECKPOINT_INTERVAL = 5
DEFAULT_RETRY_DELAY = 2
DEFAULT_MAX_TOKENS = 1000000 # 1M tokens total
DEFAULT_MAX_COST = 50.0 # $50 USD
DEFAULT_CONTEXT_WINDOW = 200000 # 200K token context window
DEFAULT_CONTEXT_THRESHOLD = 0.8 # Trigger summarization at 80% of context
DEFAULT_METRICS_INTERVAL = 10 # Log metrics every 10 iterations
DEFAULT_MAX_PROMPT_SIZE = 10485760 # 10MB max prompt file size
DEFAULT_COMPLETION_PROMISE = "LOOP_COMPLETE"
# Token costs per million (approximate)
TOKEN_COSTS = {
"claude": {"input": 3.0, "output": 15.0}, # Claude 3.5 Sonnet
"q": {"input": 0.5, "output": 1.5}, # Estimated
"kiro": {"input": 0.5, "output": 1.5}, # Estimated
"gemini": {"input": 0.5, "output": 1.5} # Gemini Pro
}
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
logger = logging.getLogger('ralph-orchestrator')
class AgentType(Enum):
"""Supported AI agent types"""
CLAUDE = "claude"
Q = "q"
KIRO = "kiro"
GEMINI = "gemini"
ACP = "acp"
AUTO = "auto"
class ConfigValidator:
"""Validates Ralph configuration settings.
Provides validation methods for configuration parameters with security
checks and warnings for unusual values.
"""
# Validation thresholds
LARGE_DELAY_THRESHOLD_SECONDS = 3600 # 1 hour
SHORT_TIMEOUT_THRESHOLD_SECONDS = 10 # Very short timeout
TYPICAL_AI_ITERATION_MIN_SECONDS = 30 # Typical minimum time for AI iteration
TYPICAL_AI_ITERATION_MAX_SECONDS = 300 # Typical maximum time for AI iteration
# Reasonable limits to prevent resource exhaustion
MAX_ITERATIONS_LIMIT = 100000
MAX_RUNTIME_LIMIT = 604800 # 1 week in seconds
MAX_TOKENS_LIMIT = 100000000 # 100M tokens
MAX_COST_LIMIT = 10000.0 # $10K USD
@staticmethod
def validate_max_iterations(max_iterations: int) -> List[str]:
"""Validate max iterations parameter."""
errors = []
if max_iterations < 0:
errors.append("Max iterations must be non-negative")
elif max_iterations > ConfigValidator.MAX_ITERATIONS_LIMIT:
errors.append(f"Max iterations exceeds limit ({ConfigValidator.MAX_ITERATIONS_LIMIT})")
return errors
@staticmethod
def validate_max_runtime(max_runtime: int) -> List[str]:
"""Validate max runtime parameter."""
errors = []
if max_runtime < 0:
errors.append("Max runtime must be non-negative")
elif max_runtime > ConfigValidator.MAX_RUNTIME_LIMIT:
errors.append(f"Max runtime exceeds limit ({ConfigValidator.MAX_RUNTIME_LIMIT}s)")
return errors
@staticmethod
def validate_checkpoint_interval(checkpoint_interval: int) -> List[str]:
"""Validate checkpoint interval parameter."""
errors = []
if checkpoint_interval < 0:
errors.append("Checkpoint interval must be non-negative")
return errors
@staticmethod
def validate_retry_delay(retry_delay: int) -> List[str]:
"""Validate retry delay parameter."""
errors = []
if retry_delay < 0:
errors.append("Retry delay must be non-negative")
elif retry_delay > ConfigValidator.LARGE_DELAY_THRESHOLD_SECONDS:
errors.append(f"Retry delay exceeds limit ({ConfigValidator.LARGE_DELAY_THRESHOLD_SECONDS}s)")
return errors
@staticmethod
def validate_max_tokens(max_tokens: int) -> List[str]:
"""Validate max tokens parameter."""
errors = []
if max_tokens < 0:
errors.append("Max tokens must be non-negative")
elif max_tokens > ConfigValidator.MAX_TOKENS_LIMIT:
errors.append(f"Max tokens exceeds limit ({ConfigValidator.MAX_TOKENS_LIMIT})")
return errors
@staticmethod
def validate_max_cost(max_cost: float) -> List[str]:
"""Validate max cost parameter."""
errors = []
if max_cost < 0:
errors.append("Max cost must be non-negative")
elif max_cost > ConfigValidator.MAX_COST_LIMIT:
errors.append(f"Max cost exceeds limit (${ConfigValidator.MAX_COST_LIMIT})")
return errors
@staticmethod
def validate_context_threshold(context_threshold: float) -> List[str]:
"""Validate context threshold parameter."""
errors = []
if not 0.0 <= context_threshold <= 1.0:
errors.append("Context threshold must be between 0.0 and 1.0")
return errors
@staticmethod
def validate_prompt_file(prompt_file: str) -> List[str]:
"""Validate prompt file exists and is readable."""
errors = []
path = Path(prompt_file)
if not path.exists():
errors.append(f"Prompt file not found: {prompt_file}")
elif not path.is_file():
errors.append(f"Prompt file is not a regular file: {prompt_file}")
return errors
@staticmethod
def get_warning_large_delay(retry_delay: int) -> List[str]:
"""Check for unusually large delay values."""
if retry_delay > ConfigValidator.LARGE_DELAY_THRESHOLD_SECONDS:
return [
f"Warning: Retry delay is very large ({retry_delay}s = {retry_delay/60:.1f}m). "
f"Did you mean to use minutes instead of seconds?"
]
return []
@staticmethod
def get_warning_single_iteration(max_iterations: int) -> List[str]:
"""Check for max_iterations=1."""
if max_iterations == 1:
return [
"Warning: max_iterations is 1. "
"Ralph is designed for continuous loops. Did you mean 0 (infinite)?"
]
return []
@staticmethod
def get_warning_short_timeout(max_runtime: int) -> List[str]:
"""Check for very short runtime limits."""
if 0 < max_runtime < ConfigValidator.SHORT_TIMEOUT_THRESHOLD_SECONDS:
return [
f"Warning: Max runtime is very short ({max_runtime}s). "
f"AI iterations typically take {ConfigValidator.TYPICAL_AI_ITERATION_MIN_SECONDS}-"
f"{ConfigValidator.TYPICAL_AI_ITERATION_MAX_SECONDS} seconds."
]
return []
@dataclass
class AdapterConfig:
"""Configuration for individual adapters"""
enabled: bool = True
args: List[str] = field(default_factory=list)
env: Dict[str, str] = field(default_factory=dict)
timeout: int = 300
max_retries: int = 3
tool_permissions: Dict[str, Any] = field(default_factory=dict)
@dataclass
class RalphConfig:
"""Configuration for Ralph orchestrator.
Thread-safe configuration class with RLock protection for mutable fields.
Provides both direct attribute access (backwards compatible) and thread-safe
getter/setter methods for concurrent access scenarios.
"""
# Core configuration fields
agent: AgentType = AgentType.AUTO
# Agent selection and fallback priority (used when agent=auto, and for fallback ordering)
# Valid values: "acp", "claude", "gemini", "qchat" (also accepts aliases: "codex"->"acp", "q"->"qchat")
agent_priority: List[str] = field(default_factory=lambda: ["claude", "kiro", "qchat", "gemini", "acp"])
prompt_file: str = DEFAULT_PROMPT_FILE
prompt_text: Optional[str] = None # Direct prompt text (overrides prompt_file)
completion_promise: Optional[str] = DEFAULT_COMPLETION_PROMISE # String to match in agent output to stop
max_iterations: int = DEFAULT_MAX_ITERATIONS
max_runtime: int = DEFAULT_MAX_RUNTIME
checkpoint_interval: int = DEFAULT_CHECKPOINT_INTERVAL
retry_delay: int = DEFAULT_RETRY_DELAY
archive_prompts: bool = True
git_checkpoint: bool = True
verbose: bool = False
dry_run: bool = False
max_tokens: int = DEFAULT_MAX_TOKENS
max_cost: float = DEFAULT_MAX_COST
context_window: int = DEFAULT_CONTEXT_WINDOW
context_threshold: float = DEFAULT_CONTEXT_THRESHOLD
metrics_interval: int = DEFAULT_METRICS_INTERVAL
enable_metrics: bool = True
max_prompt_size: int = DEFAULT_MAX_PROMPT_SIZE
allow_unsafe_paths: bool = False
agent_args: List[str] = field(default_factory=list)
adapters: Dict[str, AdapterConfig] = field(default_factory=dict)
# Output formatting configuration
output_format: str = "rich" # "plain", "rich", or "json"
output_verbosity: str = "normal" # "quiet", "normal", "verbose", "debug"
show_token_usage: bool = True # Display token usage after iterations
show_timestamps: bool = True # Include timestamps in output
# Thread safety lock - not included in initialization/equals
_lock: threading.RLock = field(
default_factory=threading.RLock, init=False, repr=False, compare=False
)
# Thread-safe property access methods for mutable fields
def get_max_iterations(self) -> int:
"""Thread-safe access to max_iterations property."""
with self._lock:
return self.max_iterations
def set_max_iterations(self, value: int) -> None:
"""Thread-safe setting of max_iterations property."""
with self._lock:
object.__setattr__(self, 'max_iterations', value)
def get_max_runtime(self) -> int:
"""Thread-safe access to max_runtime property."""
with self._lock:
return self.max_runtime
def set_max_runtime(self, value: int) -> None:
"""Thread-safe setting of max_runtime property."""
with self._lock:
object.__setattr__(self, 'max_runtime', value)
def get_checkpoint_interval(self) -> int:
"""Thread-safe access to checkpoint_interval property."""
with self._lock:
return self.checkpoint_interval
def set_checkpoint_interval(self, value: int) -> None:
"""Thread-safe setting of checkpoint_interval property."""
with self._lock:
object.__setattr__(self, 'checkpoint_interval', value)
def get_retry_delay(self) -> int:
"""Thread-safe access to retry_delay property."""
with self._lock:
return self.retry_delay
def set_retry_delay(self, value: int) -> None:
"""Thread-safe setting of retry_delay property."""
with self._lock:
object.__setattr__(self, 'retry_delay', value)
def get_max_tokens(self) -> int:
"""Thread-safe access to max_tokens property."""
with self._lock:
return self.max_tokens
def set_max_tokens(self, value: int) -> None:
"""Thread-safe setting of max_tokens property."""
with self._lock:
object.__setattr__(self, 'max_tokens', value)
def get_max_cost(self) -> float:
"""Thread-safe access to max_cost property."""
with self._lock:
return self.max_cost
def set_max_cost(self, value: float) -> None:
"""Thread-safe setting of max_cost property."""
with self._lock:
object.__setattr__(self, 'max_cost', value)
def get_verbose(self) -> bool:
"""Thread-safe access to verbose property."""
with self._lock:
return self.verbose
def set_verbose(self, value: bool) -> None:
"""Thread-safe setting of verbose property."""
with self._lock:
object.__setattr__(self, 'verbose', value)
@classmethod
def from_yaml(cls, config_path: str) -> 'RalphConfig':
"""Load configuration from YAML file."""
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
with open(config_file, 'r') as f:
config_data = yaml.safe_load(f)
# Convert agent string to AgentType enum
if 'agent' in config_data:
config_data['agent'] = AgentType(config_data['agent'])
# Process adapter configurations
if 'adapters' in config_data:
adapter_configs = {}
for name, adapter_data in config_data['adapters'].items():
if isinstance(adapter_data, dict):
adapter_configs[name] = AdapterConfig(**adapter_data)
else:
# Simple boolean enable/disable
adapter_configs[name] = AdapterConfig(enabled=bool(adapter_data))
config_data['adapters'] = adapter_configs
# Filter out unknown keys
valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
filtered_data = {k: v for k, v in config_data.items() if k in valid_keys}
return cls(**filtered_data)
def get_adapter_config(self, adapter_name: str) -> AdapterConfig:
"""Get configuration for a specific adapter."""
with self._lock:
return self.adapters.get(adapter_name, AdapterConfig())
def validate(self) -> List[str]:
"""Validate configuration settings.
Returns:
List of validation errors (empty if valid).
"""
errors = []
with self._lock:
errors.extend(ConfigValidator.validate_max_iterations(self.max_iterations))
errors.extend(ConfigValidator.validate_max_runtime(self.max_runtime))
errors.extend(ConfigValidator.validate_checkpoint_interval(self.checkpoint_interval))
errors.extend(ConfigValidator.validate_retry_delay(self.retry_delay))
errors.extend(ConfigValidator.validate_max_tokens(self.max_tokens))
errors.extend(ConfigValidator.validate_max_cost(self.max_cost))
errors.extend(ConfigValidator.validate_context_threshold(self.context_threshold))
return errors
def get_warnings(self) -> List[str]:
"""Get configuration warnings (non-blocking issues).
Returns:
List of warning messages.
"""
warnings = []
with self._lock:
warnings.extend(ConfigValidator.get_warning_large_delay(self.retry_delay))
warnings.extend(ConfigValidator.get_warning_single_iteration(self.max_iterations))
warnings.extend(ConfigValidator.get_warning_short_timeout(self.max_runtime))
return warnings
def create_output_formatter(self):
"""Create an output formatter based on configuration settings.
Returns:
OutputFormatter instance configured according to settings.
"""
from ralph_orchestrator.output import VerbosityLevel, create_formatter
# Map verbosity string to enum
verbosity_map = {
"quiet": VerbosityLevel.QUIET,
"normal": VerbosityLevel.NORMAL,
"verbose": VerbosityLevel.VERBOSE,
"debug": VerbosityLevel.DEBUG,
}
with self._lock:
verbosity = verbosity_map.get(self.output_verbosity.lower(), VerbosityLevel.NORMAL)
return create_formatter(
format_type=self.output_format,
verbosity=verbosity,
)
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Ralph Wiggum Orchestrator - Put AI in a loop until done"
)
parser.add_argument(
"--agent", "-a",
type=str,
choices=["claude", "q", "kiro", "gemini", "acp", "auto"],
default="auto",
help="AI agent to use (default: auto-detect)"
)
parser.add_argument(
"--prompt-file", "-P",
type=str,
default=DEFAULT_PROMPT_FILE,
dest="prompt",
help="Prompt file path (default: PROMPT.md)"
)
parser.add_argument(
"--prompt-text", "-p",
type=str,
default=None,
help="Direct prompt text (overrides --prompt-file)"
)
parser.add_argument(
"--completion-promise",
type=str,
default=DEFAULT_COMPLETION_PROMISE,
help=f"Stop when agent output contains this exact string (default: {DEFAULT_COMPLETION_PROMISE})"
)
parser.add_argument(
"--max-iterations", "-i",
type=int,
default=DEFAULT_MAX_ITERATIONS,
help=f"Maximum iterations (default: {DEFAULT_MAX_ITERATIONS})"
)
parser.add_argument(
"--max-runtime", "-t",
type=int,
default=DEFAULT_MAX_RUNTIME,
help=f"Maximum runtime in seconds (default: {DEFAULT_MAX_RUNTIME})"
)
parser.add_argument(
"--checkpoint-interval", "-c",
type=int,
default=DEFAULT_CHECKPOINT_INTERVAL,
help=f"Checkpoint interval (default: {DEFAULT_CHECKPOINT_INTERVAL})"
)
parser.add_argument(
"--retry-delay", "-r",
type=int,
default=DEFAULT_RETRY_DELAY,
help=f"Retry delay in seconds (default: {DEFAULT_RETRY_DELAY})"
)
parser.add_argument(
"--max-tokens",
type=int,
default=DEFAULT_MAX_TOKENS,
help=f"Maximum total tokens (default: {DEFAULT_MAX_TOKENS:,})"
)
parser.add_argument(
"--max-cost",
type=float,
default=DEFAULT_MAX_COST,
help=f"Maximum cost in USD (default: ${DEFAULT_MAX_COST:.2f})"
)
parser.add_argument(
"--context-window",
type=int,
default=DEFAULT_CONTEXT_WINDOW,
help=f"Context window size in tokens (default: {DEFAULT_CONTEXT_WINDOW:,})"
)
parser.add_argument(
"--context-threshold",
type=float,
default=DEFAULT_CONTEXT_THRESHOLD,
help=f"Context summarization threshold (default: {DEFAULT_CONTEXT_THRESHOLD:.1f} = {DEFAULT_CONTEXT_THRESHOLD*100:.0f}%%)"
)
parser.add_argument(
"--metrics-interval",
type=int,
default=DEFAULT_METRICS_INTERVAL,
help=f"Metrics logging interval (default: {DEFAULT_METRICS_INTERVAL})"
)
parser.add_argument(
"--no-metrics",
action="store_true",
help="Disable metrics collection"
)
parser.add_argument(
"--max-prompt-size",
type=int,
default=DEFAULT_MAX_PROMPT_SIZE,
help=f"Maximum prompt file size in bytes (default: {DEFAULT_MAX_PROMPT_SIZE})"
)
parser.add_argument(
"--allow-unsafe-paths",
action="store_true",
help="Allow potentially unsafe prompt paths (use with caution)"
)
parser.add_argument(
"--no-git",
action="store_true",
help="Disable git checkpointing"
)
parser.add_argument(
"--no-archive",
action="store_true",
help="Disable prompt archiving"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Enable verbose output"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Dry run mode (don't execute agents)"
)
# Output formatting options
parser.add_argument(
"--output-format",
type=str,
choices=["plain", "rich", "json"],
default="rich",
help="Output format (default: rich)"
)
parser.add_argument(
"--output-verbosity",
type=str,
choices=["quiet", "normal", "verbose", "debug"],
default="normal",
help="Output verbosity level (default: normal)"
)
parser.add_argument(
"--no-token-usage",
action="store_true",
help="Disable token usage display"
)
parser.add_argument(
"--no-timestamps",
action="store_true",
help="Disable timestamps in output"
)
parser.add_argument(
"agent_args",
nargs="*",
help="Additional arguments to pass to the AI agent"
)
args = parser.parse_args()
# Configure logging level
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Create config
config = RalphConfig(
agent=AgentType(args.agent),
prompt_file=args.prompt,
prompt_text=args.prompt_text,
max_iterations=args.max_iterations,
max_runtime=args.max_runtime,
checkpoint_interval=args.checkpoint_interval,
retry_delay=args.retry_delay,
archive_prompts=not args.no_archive,
git_checkpoint=not args.no_git,
verbose=args.verbose,
dry_run=args.dry_run,
max_tokens=args.max_tokens,
max_cost=args.max_cost,
context_window=args.context_window,
context_threshold=args.context_threshold,
metrics_interval=args.metrics_interval,
enable_metrics=not args.no_metrics,
max_prompt_size=args.max_prompt_size,
allow_unsafe_paths=args.allow_unsafe_paths,
agent_args=args.agent_args,
completion_promise=args.completion_promise,
# Output formatting options
output_format=args.output_format,
output_verbosity=args.output_verbosity,
show_token_usage=not args.no_token_usage,
show_timestamps=not args.no_timestamps,
)
# Run orchestrator
orchestrator = RalphOrchestrator(config)
return orchestrator.run()
if __name__ == "__main__":
sys.exit(main())