/ 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()