Coverage for session_buddy / core / lifecycle / session_info.py: 62.26%
80 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
1"""Session information parsing and management.
3This module provides utilities for reading, parsing, and managing session
4information from handoff files and session summaries.
5"""
7from __future__ import annotations
9from dataclasses import dataclass, field
10from typing import TYPE_CHECKING
12if TYPE_CHECKING:
13 from pathlib import Path
16@dataclass(frozen=True)
17class SessionInfo:
18 """Immutable session information."""
20 session_id: str = field(default="")
21 ended_at: str = field(default="")
22 quality_score: str = field(default="")
23 working_directory: str = field(default="")
24 top_recommendation: str = field(default="")
26 def is_complete(self) -> bool:
27 """Check if session info has required fields."""
28 return bool(self.ended_at and self.quality_score and self.working_directory)
30 @classmethod
31 def empty(cls) -> SessionInfo:
32 """Create empty session info."""
33 return cls()
35 @classmethod
36 def from_dict(cls, data: dict[str, str]) -> SessionInfo:
37 """Create from dictionary with validation."""
38 return cls( # type: ignore[call-arg]
39 session_id=data.get("session_id", ""),
40 ended_at=data.get("ended_at", ""),
41 quality_score=data.get("quality_score", ""),
42 working_directory=data.get("working_directory", ""),
43 top_recommendation=data.get("top_recommendation", ""),
44 )
47def find_latest_handoff_file(working_dir: Path) -> Path | None:
48 """Find the most recent session handoff file."""
49 try:
50 handoff_dir = working_dir / ".crackerjack" / "session" / "handoff"
52 if not handoff_dir.exists():
53 # Check for legacy handoff files in project root
54 legacy_files = list(working_dir.glob("session_handoff_*.md"))
55 if legacy_files:
56 # Return the most recent legacy file
57 return max(legacy_files, key=lambda f: f.stat().st_mtime)
58 return None
60 # Find all handoff files
61 handoff_files = list(handoff_dir.glob("session_handoff_*.md"))
63 if not handoff_files:
64 return None
66 # Return the most recent file based on timestamp in filename
67 return max(handoff_files, key=lambda f: f.name)
69 except Exception:
70 return None
73def discover_session_files(working_dir: Path) -> list[Path]:
74 """Find potential session files in priority order."""
75 candidates = [
76 working_dir / "session_handoff.md",
77 working_dir / ".claude" / "session_handoff.md",
78 working_dir / "session_summary.md",
79 ]
80 return [path for path in candidates if path.exists()]
83async def read_file_safely(file_path: Path) -> str:
84 """Read file content safely."""
85 try:
86 with file_path.open(encoding="utf-8") as f:
87 return f.read()
88 except Exception:
89 return ""
92def extract_session_metadata(lines: list[str]) -> dict[str, str]:
93 """Extract session metadata from handoff file lines."""
94 info = {}
95 for line in lines:
96 if line.startswith("**Session ended:**"):
97 info["ended_at"] = line.split("**Session ended:**")[1].strip()
98 elif line.startswith("**Final quality score:**"):
99 info["quality_score"] = line.split("**Final quality score:**")[1].strip()
100 elif line.startswith("**Working directory:**"):
101 info["working_directory"] = line.split("**Working directory:**")[1].strip()
102 return info
105def extract_session_recommendations(lines: list[str], info: dict[str, str]) -> None:
106 """Extract first recommendation from recommendations section."""
107 in_recommendations = False
108 for line in lines: 108 ↛ exitline 108 didn't return from function 'extract_session_recommendations' because the loop on line 108 didn't complete
109 if "## Recommendations for Next Session" in line:
110 in_recommendations = True
111 continue
112 if in_recommendations and line.strip().startswith("1."):
113 info["top_recommendation"] = line.strip()[3:].strip() # Remove "1. "
114 break
115 if in_recommendations and line.startswith("##"): 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true
116 break # End of recommendations section
119async def parse_session_file(file_path: Path) -> SessionInfo:
120 """Parse single session file with error handling."""
121 try:
122 content = await read_file_safely(file_path)
123 if not content: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 return SessionInfo.empty()
126 lines = content.split("\n")
127 info_dict = extract_session_metadata(lines)
128 extract_session_recommendations(lines, info_dict)
130 return SessionInfo.from_dict(info_dict)
132 except Exception:
133 return SessionInfo.empty()
136async def read_previous_session_info(handoff_file: Path) -> dict[str, str] | None:
137 """Read previous session information."""
138 try:
139 # Use the async parsing method
140 session_info = await parse_session_file(handoff_file)
142 if session_info.is_complete():
143 return {
144 "ended_at": session_info.ended_at,
145 "quality_score": session_info.quality_score,
146 "working_directory": session_info.working_directory,
147 "top_recommendation": session_info.top_recommendation,
148 }
150 return None
152 except Exception:
153 return None