claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/engineer.json +33 -11
- claude_mpm/cli/commands/agents.py +556 -1009
- claude_mpm/cli/commands/memory.py +248 -927
- claude_mpm/cli/commands/run.py +139 -484
- claude_mpm/cli/startup_logging.py +76 -0
- claude_mpm/core/agent_registry.py +6 -10
- claude_mpm/core/framework_loader.py +114 -595
- claude_mpm/core/logging_config.py +2 -4
- claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
- claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
- claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
- claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
- claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
- claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
- claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
- claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
- claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
- claude_mpm/services/agents/memory/memory_file_service.py +103 -0
- claude_mpm/services/agents/memory/memory_format_service.py +201 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
- claude_mpm/services/agents/registry/__init__.py +1 -1
- claude_mpm/services/cli/__init__.py +18 -0
- claude_mpm/services/cli/agent_cleanup_service.py +407 -0
- claude_mpm/services/cli/agent_dependency_service.py +395 -0
- claude_mpm/services/cli/agent_listing_service.py +463 -0
- claude_mpm/services/cli/agent_output_formatter.py +605 -0
- claude_mpm/services/cli/agent_validation_service.py +589 -0
- claude_mpm/services/cli/dashboard_launcher.py +424 -0
- claude_mpm/services/cli/memory_crud_service.py +617 -0
- claude_mpm/services/cli/memory_output_formatter.py +604 -0
- claude_mpm/services/cli/session_manager.py +513 -0
- claude_mpm/services/cli/socketio_manager.py +498 -0
- claude_mpm/services/cli/startup_checker.py +370 -0
- claude_mpm/services/core/cache_manager.py +311 -0
- claude_mpm/services/core/memory_manager.py +637 -0
- claude_mpm/services/core/path_resolver.py +498 -0
- claude_mpm/services/core/service_container.py +520 -0
- claude_mpm/services/core/service_interfaces.py +436 -0
- claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
- claude_mpm/cli/commands/run_config_checker.py +0 -159
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Session management service for CLI commands.
|
|
2
|
+
|
|
3
|
+
WHY: This service extracts session lifecycle management from run.py to improve
|
|
4
|
+
separation of concerns, testability, and reusability across CLI commands. It
|
|
5
|
+
handles session creation, loading, validation, and persistence.
|
|
6
|
+
|
|
7
|
+
DESIGN DECISIONS:
|
|
8
|
+
- Interface-based design for dependency injection
|
|
9
|
+
- Single responsibility: session lifecycle management
|
|
10
|
+
- Supports multiple session contexts (default, orchestration, etc.)
|
|
11
|
+
- Automatic session cleanup and archiving
|
|
12
|
+
- Thread-safe session operations
|
|
13
|
+
- Non-blocking validation with structured warnings
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import gzip
|
|
17
|
+
import json
|
|
18
|
+
import uuid
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime, timedelta
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
from claude_mpm.core.logger import get_logger
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Interface Definition
|
|
29
|
+
class ISessionManager(ABC):
|
|
30
|
+
"""Interface for session management service."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def create_session(
|
|
34
|
+
self, context: str = "default", options: Optional[Dict[str, Any]] = None
|
|
35
|
+
) -> "SessionInfo":
|
|
36
|
+
"""Create a new session with options.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
context: Session context (e.g., 'default', 'orchestration')
|
|
40
|
+
options: Optional session configuration
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
SessionInfo object with session details
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def load_session(self, session_id: str) -> Optional["SessionInfo"]:
|
|
48
|
+
"""Load an existing session by ID.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
session_id: Session UUID
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
SessionInfo if found, None otherwise
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def save_session(self, session_info: "SessionInfo") -> bool:
|
|
59
|
+
"""Persist session state to storage.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
session_info: Session to save
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if successful, False otherwise
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
70
|
+
"""Get session metadata.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
session_id: Session UUID
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Session metadata dictionary or None
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def validate_session(self, session_id: str) -> "SessionValidation":
|
|
81
|
+
"""Validate session consistency and health.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
session_id: Session UUID
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
SessionValidation with results
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def get_recent_sessions(
|
|
92
|
+
self, limit: int = 10, context: Optional[str] = None
|
|
93
|
+
) -> List["SessionInfo"]:
|
|
94
|
+
"""Get recent sessions sorted by last used.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
limit: Maximum number of sessions
|
|
98
|
+
context: Filter by context (optional)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of SessionInfo objects
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def get_last_interactive_session(self) -> Optional[str]:
|
|
106
|
+
"""Get the most recently used interactive session ID.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Session ID or None
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def record_agent_use(self, session_id: str, agent: str, task: str) -> None:
|
|
114
|
+
"""Record agent activity in session.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
session_id: Session UUID
|
|
118
|
+
agent: Agent name
|
|
119
|
+
task: Task description
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def cleanup_old_sessions(
|
|
124
|
+
self, max_age_hours: int = 24, archive: bool = True
|
|
125
|
+
) -> int:
|
|
126
|
+
"""Remove or archive old sessions.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
max_age_hours: Maximum age in hours
|
|
130
|
+
archive: Whether to archive before removing
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Number of sessions cleaned up
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def archive_sessions(self, session_ids: List[str]) -> bool:
|
|
138
|
+
"""Archive specific sessions.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
session_ids: List of session IDs to archive
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if successful, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class SessionInfo:
|
|
150
|
+
"""Session information container."""
|
|
151
|
+
|
|
152
|
+
id: str
|
|
153
|
+
context: str = "default"
|
|
154
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
155
|
+
last_used: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
156
|
+
use_count: int = 0
|
|
157
|
+
agents_run: List[Dict[str, Any]] = field(default_factory=list)
|
|
158
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
161
|
+
"""Convert to dictionary for serialization."""
|
|
162
|
+
return {
|
|
163
|
+
"id": self.id,
|
|
164
|
+
"context": self.context,
|
|
165
|
+
"created_at": self.created_at,
|
|
166
|
+
"last_used": self.last_used,
|
|
167
|
+
"use_count": self.use_count,
|
|
168
|
+
"agents_run": self.agents_run,
|
|
169
|
+
"metadata": self.metadata,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SessionInfo":
|
|
174
|
+
"""Create from dictionary."""
|
|
175
|
+
return cls(
|
|
176
|
+
id=data["id"],
|
|
177
|
+
context=data.get("context", "default"),
|
|
178
|
+
created_at=data.get("created_at", datetime.now().isoformat()),
|
|
179
|
+
last_used=data.get("last_used", datetime.now().isoformat()),
|
|
180
|
+
use_count=data.get("use_count", 0),
|
|
181
|
+
agents_run=data.get("agents_run", []),
|
|
182
|
+
metadata=data.get("metadata", {}),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class SessionValidation:
|
|
188
|
+
"""Session validation results."""
|
|
189
|
+
|
|
190
|
+
valid: bool
|
|
191
|
+
session_id: str
|
|
192
|
+
errors: List[str] = field(default_factory=list)
|
|
193
|
+
warnings: List[str] = field(default_factory=list)
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def has_issues(self) -> bool:
|
|
197
|
+
"""Check if there are any issues."""
|
|
198
|
+
return bool(self.errors or self.warnings)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class SessionManager(ISessionManager):
|
|
202
|
+
"""Service for managing Claude session lifecycle."""
|
|
203
|
+
|
|
204
|
+
def __init__(self, session_dir: Optional[Path] = None, config_service=None):
|
|
205
|
+
"""Initialize session manager.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
session_dir: Directory to store session metadata
|
|
209
|
+
config_service: Optional configuration service
|
|
210
|
+
"""
|
|
211
|
+
self.session_dir = session_dir or Path.home() / ".claude-mpm" / "sessions"
|
|
212
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
self.config_service = config_service
|
|
214
|
+
self.logger = get_logger("SessionManager")
|
|
215
|
+
self._sessions_cache: Dict[str, SessionInfo] = {}
|
|
216
|
+
self._load_sessions()
|
|
217
|
+
|
|
218
|
+
def create_session(
|
|
219
|
+
self, context: str = "default", options: Optional[Dict[str, Any]] = None
|
|
220
|
+
) -> SessionInfo:
|
|
221
|
+
"""Create a new session with options.
|
|
222
|
+
|
|
223
|
+
WHY: Creates a new session with unique ID and initializes metadata.
|
|
224
|
+
This enables session tracking and context preservation.
|
|
225
|
+
"""
|
|
226
|
+
session_id = str(uuid.uuid4())
|
|
227
|
+
|
|
228
|
+
session = SessionInfo(id=session_id, context=context, metadata=options or {})
|
|
229
|
+
|
|
230
|
+
self._sessions_cache[session_id] = session
|
|
231
|
+
self._save_sessions()
|
|
232
|
+
|
|
233
|
+
self.logger.info(f"Created session {session_id} for context: {context}")
|
|
234
|
+
return session
|
|
235
|
+
|
|
236
|
+
def load_session(self, session_id: str) -> Optional[SessionInfo]:
|
|
237
|
+
"""Load an existing session by ID.
|
|
238
|
+
|
|
239
|
+
WHY: Retrieves session state from cache or disk for resumption.
|
|
240
|
+
"""
|
|
241
|
+
# Check cache first
|
|
242
|
+
if session_id in self._sessions_cache:
|
|
243
|
+
return self._sessions_cache[session_id]
|
|
244
|
+
|
|
245
|
+
# Try loading from disk
|
|
246
|
+
self._load_sessions()
|
|
247
|
+
return self._sessions_cache.get(session_id)
|
|
248
|
+
|
|
249
|
+
def save_session(self, session_info: SessionInfo) -> bool:
|
|
250
|
+
"""Persist session state to storage.
|
|
251
|
+
|
|
252
|
+
WHY: Ensures session state is preserved across application restarts.
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
self._sessions_cache[session_info.id] = session_info
|
|
256
|
+
self._save_sessions()
|
|
257
|
+
return True
|
|
258
|
+
except Exception as e:
|
|
259
|
+
self.logger.error(f"Failed to save session {session_info.id}: {e}")
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
263
|
+
"""Get session metadata.
|
|
264
|
+
|
|
265
|
+
WHY: Provides session details for display and decision-making.
|
|
266
|
+
"""
|
|
267
|
+
session = self.load_session(session_id)
|
|
268
|
+
return session.to_dict() if session else None
|
|
269
|
+
|
|
270
|
+
def validate_session(self, session_id: str) -> SessionValidation:
|
|
271
|
+
"""Validate session consistency and health.
|
|
272
|
+
|
|
273
|
+
WHY: Ensures session data is consistent and usable before resumption.
|
|
274
|
+
Checks for corruption, missing data, and age constraints.
|
|
275
|
+
"""
|
|
276
|
+
validation = SessionValidation(valid=True, session_id=session_id)
|
|
277
|
+
|
|
278
|
+
session = self.load_session(session_id)
|
|
279
|
+
if not session:
|
|
280
|
+
validation.valid = False
|
|
281
|
+
validation.errors.append(f"Session {session_id} not found")
|
|
282
|
+
return validation
|
|
283
|
+
|
|
284
|
+
# Check session age
|
|
285
|
+
try:
|
|
286
|
+
created = datetime.fromisoformat(session.created_at)
|
|
287
|
+
age = datetime.now() - created
|
|
288
|
+
|
|
289
|
+
if age > timedelta(days=7):
|
|
290
|
+
validation.warnings.append(f"Session is {age.days} days old")
|
|
291
|
+
|
|
292
|
+
if age > timedelta(days=30):
|
|
293
|
+
validation.valid = False
|
|
294
|
+
validation.errors.append("Session too old (>30 days)")
|
|
295
|
+
except (ValueError, TypeError) as e:
|
|
296
|
+
validation.errors.append(f"Invalid timestamp: {e}")
|
|
297
|
+
validation.valid = False
|
|
298
|
+
|
|
299
|
+
# Check for required fields
|
|
300
|
+
if not session.context:
|
|
301
|
+
validation.errors.append("Missing session context")
|
|
302
|
+
validation.valid = False
|
|
303
|
+
|
|
304
|
+
# Check session file integrity
|
|
305
|
+
session_file = self.session_dir / "active_sessions.json"
|
|
306
|
+
if not session_file.exists():
|
|
307
|
+
validation.warnings.append("Session file missing, will recreate")
|
|
308
|
+
elif not session_file.stat().st_size:
|
|
309
|
+
validation.errors.append("Session file is empty")
|
|
310
|
+
validation.valid = False
|
|
311
|
+
|
|
312
|
+
return validation
|
|
313
|
+
|
|
314
|
+
def get_recent_sessions(
|
|
315
|
+
self, limit: int = 10, context: Optional[str] = None
|
|
316
|
+
) -> List[SessionInfo]:
|
|
317
|
+
"""Get recent sessions sorted by last used.
|
|
318
|
+
|
|
319
|
+
WHY: Enables users to easily resume recent sessions.
|
|
320
|
+
"""
|
|
321
|
+
sessions = list(self._sessions_cache.values())
|
|
322
|
+
|
|
323
|
+
# Filter by context if specified
|
|
324
|
+
if context:
|
|
325
|
+
sessions = [s for s in sessions if s.context == context]
|
|
326
|
+
|
|
327
|
+
# Sort by last_used descending
|
|
328
|
+
sessions.sort(key=lambda s: datetime.fromisoformat(s.last_used), reverse=True)
|
|
329
|
+
|
|
330
|
+
return sessions[:limit]
|
|
331
|
+
|
|
332
|
+
def get_last_interactive_session(self) -> Optional[str]:
|
|
333
|
+
"""Get the most recently used interactive session ID.
|
|
334
|
+
|
|
335
|
+
WHY: For --resume without arguments, we want to resume the last
|
|
336
|
+
interactive session (context="default").
|
|
337
|
+
"""
|
|
338
|
+
recent = self.get_recent_sessions(limit=1, context="default")
|
|
339
|
+
return recent[0].id if recent else None
|
|
340
|
+
|
|
341
|
+
def record_agent_use(self, session_id: str, agent: str, task: str) -> None:
|
|
342
|
+
"""Record agent activity in session.
|
|
343
|
+
|
|
344
|
+
WHY: Tracks which agents were used in a session for context
|
|
345
|
+
preservation and debugging.
|
|
346
|
+
"""
|
|
347
|
+
session = self.load_session(session_id)
|
|
348
|
+
if not session:
|
|
349
|
+
self.logger.warning(
|
|
350
|
+
f"Cannot record agent use: session {session_id} not found"
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
session.agents_run.append(
|
|
355
|
+
{
|
|
356
|
+
"agent": agent,
|
|
357
|
+
"task": task[:100], # Truncate long tasks
|
|
358
|
+
"timestamp": datetime.now().isoformat(),
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
session.last_used = datetime.now().isoformat()
|
|
362
|
+
session.use_count += 1
|
|
363
|
+
|
|
364
|
+
self.save_session(session)
|
|
365
|
+
|
|
366
|
+
def cleanup_old_sessions(
|
|
367
|
+
self, max_age_hours: int = 24, archive: bool = True
|
|
368
|
+
) -> int:
|
|
369
|
+
"""Remove or archive old sessions.
|
|
370
|
+
|
|
371
|
+
WHY: Prevents unbounded growth of session data and improves performance.
|
|
372
|
+
"""
|
|
373
|
+
now = datetime.now()
|
|
374
|
+
max_age = timedelta(hours=max_age_hours)
|
|
375
|
+
|
|
376
|
+
expired_ids = []
|
|
377
|
+
for session_id, session in self._sessions_cache.items():
|
|
378
|
+
try:
|
|
379
|
+
created = datetime.fromisoformat(session.created_at)
|
|
380
|
+
if now - created > max_age:
|
|
381
|
+
expired_ids.append(session_id)
|
|
382
|
+
except (ValueError, TypeError):
|
|
383
|
+
# Invalid timestamp, mark for cleanup
|
|
384
|
+
expired_ids.append(session_id)
|
|
385
|
+
|
|
386
|
+
if archive and expired_ids:
|
|
387
|
+
self.archive_sessions(expired_ids)
|
|
388
|
+
|
|
389
|
+
# Remove from cache
|
|
390
|
+
for session_id in expired_ids:
|
|
391
|
+
del self._sessions_cache[session_id]
|
|
392
|
+
self.logger.info(f"Cleaned up expired session: {session_id}")
|
|
393
|
+
|
|
394
|
+
if expired_ids:
|
|
395
|
+
self._save_sessions()
|
|
396
|
+
|
|
397
|
+
return len(expired_ids)
|
|
398
|
+
|
|
399
|
+
def archive_sessions(self, session_ids: List[str]) -> bool:
|
|
400
|
+
"""Archive specific sessions.
|
|
401
|
+
|
|
402
|
+
WHY: Preserves session history while reducing active memory usage.
|
|
403
|
+
"""
|
|
404
|
+
if not session_ids:
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
archive_dir = self.session_dir.parent / "archives" / "sessions"
|
|
408
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
|
|
410
|
+
# Collect sessions to archive
|
|
411
|
+
sessions_to_archive = []
|
|
412
|
+
for sid in session_ids:
|
|
413
|
+
session = self.load_session(sid)
|
|
414
|
+
if session:
|
|
415
|
+
sessions_to_archive.append(session.to_dict())
|
|
416
|
+
|
|
417
|
+
if not sessions_to_archive:
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
# Create timestamped archive file
|
|
421
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
422
|
+
archive_name = f"sessions_archive_{timestamp}.json.gz"
|
|
423
|
+
archive_path = archive_dir / archive_name
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
# Compress and save sessions
|
|
427
|
+
with gzip.open(archive_path, "wt", encoding="utf-8") as f:
|
|
428
|
+
json.dump(sessions_to_archive, f, indent=2)
|
|
429
|
+
|
|
430
|
+
self.logger.info(
|
|
431
|
+
f"Archived {len(sessions_to_archive)} sessions to {archive_path}"
|
|
432
|
+
)
|
|
433
|
+
return True
|
|
434
|
+
except Exception as e:
|
|
435
|
+
self.logger.error(f"Failed to archive sessions: {e}")
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
def _save_sessions(self) -> None:
|
|
439
|
+
"""Save sessions to disk.
|
|
440
|
+
|
|
441
|
+
WHY: Persists session state for recovery after restart.
|
|
442
|
+
"""
|
|
443
|
+
session_file = self.session_dir / "active_sessions.json"
|
|
444
|
+
try:
|
|
445
|
+
sessions_dict = {
|
|
446
|
+
sid: session.to_dict() for sid, session in self._sessions_cache.items()
|
|
447
|
+
}
|
|
448
|
+
with open(session_file, "w") as f:
|
|
449
|
+
json.dump(sessions_dict, f, indent=2)
|
|
450
|
+
except Exception as e:
|
|
451
|
+
self.logger.error(f"Failed to save sessions: {e}")
|
|
452
|
+
|
|
453
|
+
def _load_sessions(self) -> None:
|
|
454
|
+
"""Load sessions from disk.
|
|
455
|
+
|
|
456
|
+
WHY: Restores session state from persistent storage.
|
|
457
|
+
"""
|
|
458
|
+
session_file = self.session_dir / "active_sessions.json"
|
|
459
|
+
if not session_file.exists():
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
with open(session_file) as f:
|
|
464
|
+
sessions_dict = json.load(f)
|
|
465
|
+
|
|
466
|
+
self._sessions_cache = {
|
|
467
|
+
sid: SessionInfo.from_dict(data) for sid, data in sessions_dict.items()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
# Clean up old sessions on load
|
|
471
|
+
self.cleanup_old_sessions(archive=True)
|
|
472
|
+
except Exception as e:
|
|
473
|
+
self.logger.error(f"Failed to load sessions: {e}")
|
|
474
|
+
self._sessions_cache = {}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# Context manager for session management
|
|
478
|
+
class ManagedSession:
|
|
479
|
+
"""Context manager for session lifecycle.
|
|
480
|
+
|
|
481
|
+
WHY: Provides a clean interface for session management with automatic
|
|
482
|
+
cleanup and error handling.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
def __init__(
|
|
486
|
+
self,
|
|
487
|
+
manager: ISessionManager,
|
|
488
|
+
context: str = "default",
|
|
489
|
+
options: Optional[Dict[str, Any]] = None,
|
|
490
|
+
):
|
|
491
|
+
"""Initialize managed session.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
manager: Session manager instance
|
|
495
|
+
context: Session context
|
|
496
|
+
options: Optional session configuration
|
|
497
|
+
"""
|
|
498
|
+
self.manager = manager
|
|
499
|
+
self.context = context
|
|
500
|
+
self.options = options
|
|
501
|
+
self.session: Optional[SessionInfo] = None
|
|
502
|
+
|
|
503
|
+
def __enter__(self) -> SessionInfo:
|
|
504
|
+
"""Enter session context, return session info."""
|
|
505
|
+
self.session = self.manager.create_session(self.context, self.options)
|
|
506
|
+
return self.session
|
|
507
|
+
|
|
508
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
509
|
+
"""Exit session context with cleanup."""
|
|
510
|
+
if self.session:
|
|
511
|
+
# Update last used time
|
|
512
|
+
self.session.last_used = datetime.now().isoformat()
|
|
513
|
+
self.manager.save_session(self.session)
|