""" Session model for tracking individual development sessions. """ from datetime import datetime from typing import Optional, List, Dict, Any from sqlalchemy import String, Text, Integer, DateTime, JSON, ForeignKey, Boolean from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from .base import Base, TimestampMixin class Session(Base, TimestampMixin): """ Represents an individual development session within a project. A session starts when Claude Code is launched or resumed and ends when the user stops or the session is interrupted. """ __tablename__ = "sessions" # Primary key id: Mapped[int] = mapped_column(primary_key=True) # Foreign key to project project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"), nullable=False, index=True) # Session timing start_time: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=func.now(), index=True ) end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) # Session metadata session_type: Mapped[str] = mapped_column(String(50), nullable=False) # startup, resume, clear working_directory: Mapped[str] = mapped_column(Text, nullable=False) git_branch: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) environment: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True) # Session statistics (updated as session progresses) duration_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) activity_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) conversation_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) files_touched: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True) # Relationships project: Mapped["Project"] = relationship("Project", back_populates="sessions") conversations: Mapped[List["Conversation"]] = relationship( "Conversation", back_populates="session", cascade="all, delete-orphan", order_by="Conversation.timestamp" ) activities: Mapped[List["Activity"]] = relationship( "Activity", back_populates="session", cascade="all, delete-orphan", order_by="Activity.timestamp" ) waiting_periods: Mapped[List["WaitingPeriod"]] = relationship( "WaitingPeriod", back_populates="session", cascade="all, delete-orphan", order_by="WaitingPeriod.start_time" ) git_operations: Mapped[List["GitOperation"]] = relationship( "GitOperation", back_populates="session", cascade="all, delete-orphan", order_by="GitOperation.timestamp" ) def __repr__(self) -> str: return f"" @property def is_active(self) -> bool: """Check if this session is still active (not ended).""" return self.end_time is None @property def calculated_duration_minutes(self) -> Optional[int]: """Calculate session duration in minutes.""" if self.end_time is None: # Session is still active, calculate current duration current_duration = datetime.utcnow() - self.start_time return int(current_duration.total_seconds() / 60) else: # Session is finished if self.duration_minutes is not None: return self.duration_minutes else: duration = self.end_time - self.start_time return int(duration.total_seconds() / 60) def end_session(self, end_reason: str = "normal") -> None: """End the session and calculate final statistics.""" if self.end_time is None: self.end_time = func.now() self.duration_minutes = self.calculated_duration_minutes # Update project statistics if self.project: unique_files = len(set(self.files_touched or [])) total_lines = sum( (activity.lines_added or 0) + (activity.lines_removed or 0) for activity in self.activities ) self.project.update_stats( session_duration_minutes=self.duration_minutes or 0, files_count=unique_files, lines_count=total_lines ) def add_activity(self) -> None: """Increment activity counter.""" self.activity_count += 1 def add_conversation(self) -> None: """Increment conversation counter.""" self.conversation_count += 1 def add_file_touched(self, file_path: str) -> None: """Add a file to the list of files touched in this session.""" if self.files_touched is None: self.files_touched = [] if file_path not in self.files_touched: self.files_touched.append(file_path)