/ run_alpha_replay.py
run_alpha_replay.py
1 #!/usr/bin/env python3 2 """ 3 Alpha Replay - Process historical conversations through Mission Control. 4 5 Usage: 6 python run_alpha_replay.py --date 2026-01-14 7 8 # Or specify conversation logs directory: 9 python run_alpha_replay.py --logs ~/.claude/projects/-Users-rcerf-repos/ 10 11 # With specific output directory: 12 python run_alpha_replay.py --date 2026-01-14 --output ./sessions 13 14 This script: 15 1. Finds JSONL conversation logs from the specified date 16 2. Parses each conversation to extract patterns 17 3. Generates LIVE-COMPRESSION-replay-*.md files 18 4. Optionally triggers Mission Control synthesis 19 """ 20 21 import sys 22 import os 23 import argparse 24 import logging 25 from pathlib import Path 26 from datetime import datetime, timedelta 27 from typing import List, Tuple 28 29 # Ensure user site-packages is in path 30 user_site = Path.home() / "Library/Python/3.9/lib/python/site-packages" 31 if user_site.exists() and str(user_site) not in sys.path: 32 sys.path.insert(0, str(user_site)) 33 34 # Add core to path 35 sys.path.insert(0, str(Path(__file__).parent)) 36 37 from core.replay import ( 38 ConversationParser, 39 ConversationThread, 40 summarize_thread, 41 CompressionGenerator, 42 CompressionConfig, 43 ) 44 45 46 def setup_logging(debug: bool = False) -> None: 47 """Configure logging.""" 48 level = logging.DEBUG if debug else logging.INFO 49 logging.basicConfig( 50 level=level, 51 format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", 52 datefmt="%H:%M:%S" 53 ) 54 55 56 def print_banner() -> None: 57 """Print startup banner.""" 58 print(""" 59 ╔════════════════════════════════════════════════════════════════╗ 60 ║ ║ 61 ║ █████╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ║ 62 ║ ██╔══██╗██║ ██╔══██╗██║ ██║██╔══██╗ ║ 63 ║ ███████║██║ ██████╔╝███████║███████║ ║ 64 ║ ██╔══██║██║ ██╔═══╝ ██╔══██║██╔══██║ ║ 65 ║ ██║ ██║███████╗██║ ██║ ██║██║ ██║ ║ 66 ║ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║ 67 ║ ║ 68 ║ ██████╗ ███████╗██████╗ ██╗ █████╗ ██╗ ██╗ ║ 69 ║ ██╔══██╗██╔════╝██╔══██╗██║ ██╔══██╗╚██╗ ██╔╝ ║ 70 ║ ██████╔╝█████╗ ██████╔╝██║ ███████║ ╚████╔╝ ║ 71 ║ ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██╔══██║ ╚██╔╝ ║ 72 ║ ██║ ██║███████╗██║ ███████╗██║ ██║ ██║ ║ 73 ║ ╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ║ 74 ║ ║ 75 ║ SOVEREIGN OS - Historical Session Replay ║ 76 ║ ║ 77 ╚════════════════════════════════════════════════════════════════╝ 78 """) 79 80 81 def find_logs_for_date(logs_dir: Path, target_date: datetime) -> List[Tuple[Path, int]]: 82 """Find conversation logs modified on the target date.""" 83 logs = [] 84 85 if not logs_dir.exists(): 86 logging.warning(f"Logs directory not found: {logs_dir}") 87 return logs 88 89 # Look for JSONL files 90 for jsonl_file in logs_dir.glob("*.jsonl"): 91 try: 92 stat = jsonl_file.stat() 93 mtime = datetime.fromtimestamp(stat.st_mtime) 94 95 # Check if modified on target date 96 if mtime.date() == target_date.date(): 97 size = stat.st_size 98 if size > 1000: # Skip tiny files 99 logs.append((jsonl_file, size)) 100 except (OSError, ValueError): 101 continue 102 103 # Sort by size descending (larger files likely more content) 104 logs.sort(key=lambda x: x[1], reverse=True) 105 return logs 106 107 108 def analyze_logs(logs: List[Tuple[Path, int]]) -> List[ConversationThread]: 109 """Parse all log files and return conversation threads.""" 110 parser = ConversationParser() 111 threads = [] 112 113 for log_path, size in logs: 114 logging.info(f"Parsing: {log_path.name} ({size / 1024:.1f} KB)") 115 try: 116 thread = parser.parse_jsonl(log_path) 117 if thread.exchange_count > 0: 118 threads.append(thread) 119 logging.info(f" -> {thread.exchange_count} exchanges, {len(thread.topics)} topics") 120 else: 121 logging.warning(f" -> No exchanges found") 122 except Exception as e: 123 logging.error(f" -> Error parsing: {e}") 124 125 return threads 126 127 128 def generate_compressions(threads: List[ConversationThread], output_dir: Path) -> List[Path]: 129 """Generate LIVE-COMPRESSION files for all threads.""" 130 config = CompressionConfig( 131 output_dir=output_dir, 132 thread_prefix="replay", 133 include_artifacts=True, 134 include_decisions=True, 135 ) 136 137 generator = CompressionGenerator(config) 138 return generator.generate_batch(threads) 139 140 141 def generate_synthesis(threads: List[ConversationThread], output_dir: Path, target_date: datetime) -> Path: 142 """Generate a historical DAILY-SYNTHESIS file.""" 143 date_str = target_date.strftime('%Y-%m-%d') 144 synthesis_path = output_dir / f"DAILY-SYNTHESIS-{date_str}.md" 145 146 # Calculate totals 147 total_exchanges = sum(t.exchange_count for t in threads) 148 all_topics = set() 149 all_artifacts = set() 150 all_decisions = set() 151 152 for t in threads: 153 all_topics.update(t.topics) 154 all_artifacts.update(t.artifacts) 155 all_decisions.update(t.decisions) 156 157 lines = [ 158 f"# Daily Synthesis - {date_str}", 159 "", 160 "*Mission Control synthesis from Alpha Replay*", 161 "", 162 f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M')}", 163 f"**Source Date:** {date_str}", 164 f"**Threads Processed:** {len(threads)}", 165 f"**Total Exchanges:** {total_exchanges}", 166 "", 167 "---", 168 "", 169 "## Thread Overview", 170 "", 171 "| Thread | Exchanges | Duration | Primary Focus |", 172 "|--------|-----------|----------|---------------|", 173 ] 174 175 for thread in threads: 176 name = thread.thread_id[:8] 177 exchanges = thread.exchange_count 178 duration = f"{thread.duration_minutes:.0f}m" if thread.duration_minutes else "-" 179 focus = thread.topics[0][:40] if thread.topics else "-" 180 lines.append(f"| {name} | {exchanges} | {duration} | {focus} |") 181 182 lines.extend(["", "---", "", "## Cross-Thread Patterns", ""]) 183 184 # Find CONCEPTS that appear in multiple threads (semantic resonance) 185 concept_counts = {} 186 for t in threads: 187 thread_concepts = getattr(t, 'concepts', []) 188 for concept in thread_concepts: 189 if concept not in concept_counts: 190 concept_counts[concept] = [] 191 concept_counts[concept].append(t.thread_id[:8]) 192 193 shared_concepts = [(concept, thread_ids) for concept, thread_ids in concept_counts.items() if len(thread_ids) > 1] 194 shared_concepts.sort(key=lambda x: len(x[1]), reverse=True) # Most shared first 195 196 if shared_concepts: 197 lines.append("### Resonant Concepts (shared across threads)") 198 lines.append("") 199 lines.append("| Concept | Thread Count | Threads |") 200 lines.append("|---------|--------------|---------|") 201 for concept, thread_ids in shared_concepts[:15]: 202 lines.append(f"| **{concept}** | {len(thread_ids)} | {', '.join(thread_ids)} |") 203 204 # Group threads by concept overlap 205 lines.extend(["", "### Concept Clusters", ""]) 206 207 # Find threads with high concept overlap 208 thread_concepts_map = {t.thread_id[:8]: set(getattr(t, 'concepts', [])) for t in threads} 209 for i, t1 in enumerate(threads): 210 for t2 in threads[i+1:]: 211 t1_id, t2_id = t1.thread_id[:8], t2.thread_id[:8] 212 overlap = thread_concepts_map[t1_id] & thread_concepts_map[t2_id] 213 if len(overlap) >= 3: # Significant overlap 214 lines.append(f"- **{t1_id} ↔ {t2_id}**: {', '.join(sorted(overlap))}") 215 else: 216 lines.append("*No cross-thread resonance detected*") 217 218 # Also show topic-based resonance (original) 219 topic_counts = {} 220 for t in threads: 221 for topic in t.topics: 222 topic_lower = topic.lower() 223 if topic_lower not in topic_counts: 224 topic_counts[topic_lower] = {'topic': topic, 'threads': []} 225 topic_counts[topic_lower]['threads'].append(t.thread_id[:8]) 226 227 shared_topics = [(v['topic'], v['threads']) for v in topic_counts.values() if len(v['threads']) > 1] 228 229 if shared_topics: 230 lines.extend(["", "### Shared Topics (exact match)", ""]) 231 for topic, thread_ids in shared_topics[:10]: 232 lines.append(f"- **{topic}** → {', '.join(thread_ids)}") 233 234 # Flow state detection 235 flow_threads = [(t, t.flow_moments, t.peak_engagement) for t in threads if getattr(t, 'flow_moments', [])] 236 if flow_threads: 237 lines.extend(["", "---", "", "## Flow State Moments", ""]) 238 lines.append("*High-engagement moments detected via signal word analysis*") 239 lines.append("") 240 241 for thread, moments, peak in flow_threads: 242 lines.append(f"### Thread {thread.thread_id[:8]} (peak engagement: {peak:.2f}x)") 243 for moment in moments[:5]: # Top 5 per thread 244 time_str = moment.timestamp.strftime('%H:%M') if moment.timestamp else '??:??' 245 tags_str = ', '.join(moment.tags) if moment.tags else '-' 246 lines.append(f"- **{time_str}** [{tags_str}] (weight: {moment.weight_modifier:.2f}x)") 247 lines.append(f" > {moment.text[:100]}...") 248 lines.append("") 249 250 lines.extend(["", "---", "", "## All Topics", ""]) 251 for topic in list(all_topics)[:20]: 252 lines.append(f"- {topic}") 253 254 lines.extend(["", "---", "", "## Artifacts Created", ""]) 255 for artifact in list(all_artifacts)[:20]: 256 lines.append(f"- `{artifact}`") 257 258 lines.extend(["", "---", "", "## Decisions Made", ""]) 259 for decision in list(all_decisions)[:15]: 260 lines.append(f"- {decision}") 261 262 lines.extend([ 263 "", 264 "---", 265 "", 266 "## Resurrection Seed", 267 "", 268 f"**{date_str} Summary:**", 269 "", 270 f"- {len(threads)} concurrent sessions", 271 f"- {total_exchanges} total exchanges", 272 f"- {len(all_topics)} unique topics", 273 f"- {len(all_artifacts)} artifacts created", 274 "", 275 "---", 276 "", 277 f"*Alpha Replay Synthesis | {date_str} | Generated {datetime.now().strftime('%H:%M')}*", 278 ]) 279 280 synthesis_path.write_text('\n'.join(lines)) 281 return synthesis_path 282 283 284 def main(): 285 parser = argparse.ArgumentParser( 286 description="Alpha Replay - Process historical conversations through Mission Control" 287 ) 288 parser.add_argument( 289 "--date", 290 type=str, 291 default="2026-01-14", 292 help="Target date (YYYY-MM-DD) to replay (default: 2026-01-14)" 293 ) 294 parser.add_argument( 295 "--logs", 296 type=Path, 297 default=Path.home() / ".claude/projects/-Users-rcerf-repos/", 298 help="Path to Claude conversation logs directory" 299 ) 300 parser.add_argument( 301 "--output", 302 type=Path, 303 default=Path(__file__).parent / "sessions", 304 help="Output directory for generated files (default: ./sessions)" 305 ) 306 parser.add_argument( 307 "--debug", 308 action="store_true", 309 help="Enable debug logging" 310 ) 311 parser.add_argument( 312 "--no-banner", 313 action="store_true", 314 help="Skip startup banner" 315 ) 316 parser.add_argument( 317 "--dry-run", 318 action="store_true", 319 help="Analyze logs but don't generate files" 320 ) 321 322 args = parser.parse_args() 323 324 # Setup 325 setup_logging(args.debug) 326 327 if not args.no_banner: 328 print_banner() 329 330 # Parse target date 331 try: 332 target_date = datetime.strptime(args.date, "%Y-%m-%d") 333 except ValueError: 334 print(f"Error: Invalid date format: {args.date}") 335 print("Use YYYY-MM-DD format") 336 sys.exit(1) 337 338 print(f"Target date: {target_date.strftime('%Y-%m-%d')}") 339 print(f"Logs directory: {args.logs}") 340 print(f"Output directory: {args.output}") 341 print() 342 343 # Find logs 344 print("Scanning for conversation logs...") 345 logs = find_logs_for_date(args.logs, target_date) 346 347 if not logs: 348 print(f"No conversation logs found for {target_date.strftime('%Y-%m-%d')}") 349 print(f"Checked: {args.logs}") 350 sys.exit(1) 351 352 print(f"Found {len(logs)} conversation logs:") 353 for log_path, size in logs: 354 print(f" - {log_path.name} ({size / 1024:.1f} KB)") 355 print() 356 357 # Parse logs 358 print("Parsing conversations...") 359 threads = analyze_logs(logs) 360 361 if not threads: 362 print("No valid conversations found") 363 sys.exit(1) 364 365 print(f"\nParsed {len(threads)} conversation threads:") 366 for thread in threads: 367 print(f"\n{summarize_thread(thread)}") 368 369 if args.dry_run: 370 print("\n[Dry run - no files generated]") 371 return 372 373 # Generate compressions 374 print("\n" + "="*60) 375 print("Generating LIVE-COMPRESSION files...") 376 compression_paths = generate_compressions(threads, args.output) 377 378 print(f"\nGenerated {len(compression_paths)} compression files:") 379 for path in compression_paths: 380 print(f" - {path.name}") 381 382 # Generate synthesis 383 print("\nGenerating daily synthesis...") 384 synthesis_path = generate_synthesis(threads, args.output, target_date) 385 print(f" - {synthesis_path.name}") 386 387 print("\n" + "="*60) 388 print("Alpha Replay complete!") 389 print(f"\nTo view results:") 390 print(f" open {args.output}") 391 print(f"\nTo run Mission Control on these files:") 392 print(f" python run_mission_control.py --sessions {args.output}") 393 394 395 if __name__ == "__main__": 396 main()