Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions .claude/hooks/devstream/memory/post_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,12 +1012,11 @@ async def update_session_tracking(
if file_path not in current_files:
current_files.append(file_path)

# DISABLED: WorkSessionManager.update_session_progress() doesn't accept active_files
# Event Sourcing captures this via capture_session_event() instead
# await session_manager.update_session_progress(
# session_id=session_id,
# active_files=current_files
# )
# Update session with active_files via WorkSessionManager
await session_manager.update_session_progress(
session_id=session_id,
active_files=current_files
)

self.base.debug_log(
f"Updated active_files via WorkSessionManager: {file_path} "
Expand All @@ -1042,13 +1041,12 @@ async def update_session_tracking(
current_tasks.append(task_content)
tasks_updated = True

# DISABLED: WorkSessionManager.update_session_progress() doesn't accept active_tasks
# Event Sourcing captures this via capture_session_event() instead
# if tasks_updated:
# await session_manager.update_session_progress(
# session_id=session_id,
# active_tasks=current_tasks
# )
# Update session with active_tasks via WorkSessionManager
if tasks_updated:
await session_manager.update_session_progress(
session_id=session_id,
active_tasks=current_tasks
)

self.base.debug_log(
f"Updated active_tasks via WorkSessionManager: "
Expand Down
30 changes: 30 additions & 0 deletions .claude/hooks/devstream/sessions/session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ async def initialize_session(self, session_id: str) -> Dict[str, Any]:
}

try:
# Session ID-based idempotency check (v2 - multi-session safe)
# Check if session already exists and is active before cleanup
existing_session = await self.session_manager.get_session(session_id)

if existing_session and existing_session.status == "active":
self.logger.info(
f"Session {session_id[:12]}... already initialized - idempotent return"
)

# Update last_activity_at and return existing session
await self.session_manager.resume_session(session_id)

# Bind context for automatic log propagation
self.session_manager.bind_session_context(
session_id=existing_session.id,
session_name=existing_session.session_name
)

results["success"] = True
results["session_resumed"] = True
results["session_data"] = {
"id": existing_session.id,
"status": existing_session.status,
"started_at": existing_session.started_at.isoformat(),
"tokens_used": existing_session.tokens_used
}

self.logger.debug(f"Idempotent return for active session: {session_id[:12]}...")
return results

# Proactive cleanup of zombie sessions before checking limits
self.logger.info("Performing proactive session cleanup...")
cleanup_stats = self.cleanup_manager.aggressive_cleanup()
Expand Down
53 changes: 52 additions & 1 deletion .claude/hooks/devstream/sessions/session_summary_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class SessionSummary:
started_at: datetime
ended_at: datetime
duration_minutes: int
duration_formatted: str # Human-readable duration (e.g., "2 hours 15 minutes", "45 seconds")

# Work accomplished
tasks_completed: int
Expand Down Expand Up @@ -78,7 +79,7 @@ def to_markdown(self) -> str:
**Session**: {self.session_name or self.session_id}
**Started**: {started}
**Ended**: {ended}
**Duration**: {self.duration_minutes} minutes
**Duration**: {self.duration_formatted}
**Status**: {self.status}

---
Expand Down Expand Up @@ -165,6 +166,51 @@ def __init__(self):

self.logger.info("SessionSummaryGenerator initialized")

def _format_duration(self, started_at: datetime, ended_at: datetime) -> str:
"""
Format session duration in human-readable format.

Handles short sessions (<1 minute) by showing seconds instead of "0 minutes".

Args:
started_at: Session start timestamp
ended_at: Session end timestamp

Returns:
Human-readable duration string (e.g., "2 hours 15 minutes", "45 seconds", "1 second")

Examples:
>>> _format_duration(datetime(2025, 1, 1, 10, 0, 0), datetime(2025, 1, 1, 10, 0, 30))
"30 seconds"
>>> _format_duration(datetime(2025, 1, 1, 10, 0, 0), datetime(2025, 1, 1, 10, 5, 0))
"5 minutes"
>>> _format_duration(datetime(2025, 1, 1, 10, 0, 0), datetime(2025, 1, 1, 12, 15, 0))
"2 hours 15 minutes"
"""
if not ended_at or not started_at:
return "0 minutes"

duration_seconds = int((ended_at - started_at).total_seconds())

# Less than 1 minute → show seconds
if duration_seconds < 60:
return f"{duration_seconds} second{'s' if duration_seconds != 1 else ''}"

# Less than 1 hour → show minutes
elif duration_seconds < 3600:
minutes = duration_seconds // 60
return f"{minutes} minute{'s' if minutes != 1 else ''}"

# 1 hour or more → show hours + minutes
else:
hours = duration_seconds // 3600
minutes = (duration_seconds % 3600) // 60
parts = []
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0:
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
return " ".join(parts)

def aggregate_session_data(
self,
session_data: SessionData,
Expand Down Expand Up @@ -201,10 +247,14 @@ def aggregate_session_data(
duration = ended_at - session_data.started_at
duration_minutes = int(duration.total_seconds() / 60)

# Format duration for human readability (handles short sessions)
duration_formatted = self._format_duration(session_data.started_at, ended_at)

self.logger.debug(
"Aggregating session data",
session_id=session_data.session_id,
duration_minutes=duration_minutes,
duration_formatted=duration_formatted,
tasks_completed=task_stats.completed if task_stats else 0,
files_modified=memory_stats.files_modified if memory_stats else 0
)
Expand All @@ -216,6 +266,7 @@ def aggregate_session_data(
started_at=session_data.started_at,
ended_at=ended_at,
duration_minutes=duration_minutes,
duration_formatted=duration_formatted,

# Work accomplished
tasks_completed=task_stats.completed if task_stats else 0,
Expand Down
8 changes: 7 additions & 1 deletion .claude/hooks/devstream/sessions/work_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ async def update_session_progress(
session_id: str,
tokens_delta: int = 0,
active_tasks: Optional[List[str]] = None,
completed_tasks: Optional[List[str]] = None
completed_tasks: Optional[List[str]] = None,
active_files: Optional[List[str]] = None
) -> bool:
"""
Update session progress metrics.
Expand All @@ -311,6 +312,7 @@ async def update_session_progress(
tokens_delta: Token count increment (added to existing tokens_used)
active_tasks: Current active tasks list (replaces existing)
completed_tasks: Current completed tasks list (replaces existing)
active_files: Current active files list (replaces existing)

Returns:
bool: True if update successful
Expand Down Expand Up @@ -338,6 +340,10 @@ async def update_session_progress(
updates.append("completed_tasks = ?")
params.append(json.dumps(completed_tasks))

if active_files is not None:
updates.append("active_files = ?")
params.append(json.dumps(active_files))

# Add session_id for WHERE clause
params.append(session_id)

Expand Down