claude-memory-agent 2.2.2 → 2.2.4

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.
@@ -0,0 +1,181 @@
1
+ """Cross-session awareness service.
2
+
3
+ Provides higher-level operations for tracking concurrent Claude Code sessions,
4
+ detecting file conflicts, and enabling session catch-up. Wraps DatabaseService
5
+ methods with business logic.
6
+
7
+ Usage:
8
+ from services.session_awareness import get_session_awareness
9
+ awareness = get_session_awareness(db)
10
+ result = await awareness.register_session(session_id, project_path)
11
+ """
12
+ import json
13
+ import logging
14
+ from datetime import datetime
15
+ from typing import Dict, Any, Optional, List
16
+
17
+ from services.database import DatabaseService
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ _instance: Optional["SessionAwarenessService"] = None
22
+
23
+
24
+ def get_session_awareness(db: DatabaseService) -> "SessionAwarenessService":
25
+ """Get or create the singleton SessionAwarenessService."""
26
+ global _instance
27
+ if _instance is None or _instance.db is not db:
28
+ _instance = SessionAwarenessService(db)
29
+ return _instance
30
+
31
+
32
+ class SessionAwarenessService:
33
+ """High-level service for cross-session awareness.
34
+
35
+ Wraps raw DB methods with business logic like:
36
+ - Auto-posting activity events on register/deregister
37
+ - Returning siblings + conflicts on heartbeat
38
+ - Grouping catch-up events by session
39
+ """
40
+
41
+ def __init__(self, db: DatabaseService):
42
+ self.db = db
43
+
44
+ async def register_session(
45
+ self, session_id: str, project_path: str,
46
+ goal: Optional[str] = None, label: Optional[str] = None
47
+ ) -> Dict[str, Any]:
48
+ """Register a session, post session_start activity, return active siblings."""
49
+ await self.db.register_active_session(session_id, project_path, goal, label)
50
+
51
+ await self.db.post_session_activity(
52
+ session_id, project_path, "session_start",
53
+ f"Session started{': ' + goal if goal else ''}"
54
+ )
55
+
56
+ siblings = await self.db.get_active_sessions(project_path, exclude_session_id=session_id)
57
+
58
+ return {
59
+ "success": True,
60
+ "session_id": session_id,
61
+ "active_siblings": siblings,
62
+ "sibling_count": len(siblings),
63
+ }
64
+
65
+ async def heartbeat(
66
+ self, session_id: str, project_path: str,
67
+ files_modified: Optional[List[str]] = None,
68
+ current_goal: Optional[str] = None,
69
+ key_decisions: Optional[List[str]] = None,
70
+ summary: Optional[str] = None
71
+ ) -> Dict[str, Any]:
72
+ """Update heartbeat and return siblings + file conflicts."""
73
+ await self.db.heartbeat_session(
74
+ session_id, files_modified, current_goal, key_decisions, summary
75
+ )
76
+
77
+ siblings = await self.db.get_active_sessions(project_path, exclude_session_id=session_id)
78
+ conflicts = await self.db.detect_file_conflicts(session_id, project_path)
79
+
80
+ return {
81
+ "success": True,
82
+ "active_siblings": siblings,
83
+ "sibling_count": len(siblings),
84
+ "file_conflicts": conflicts,
85
+ "has_conflicts": len(conflicts) > 0,
86
+ }
87
+
88
+ async def deregister_session(
89
+ self, session_id: str, project_path: str,
90
+ final_summary: Optional[str] = None
91
+ ) -> Dict[str, Any]:
92
+ """Mark session completed and post session_end activity."""
93
+ if final_summary:
94
+ await self.db.heartbeat_session(session_id, summary=final_summary)
95
+
96
+ await self.db.post_session_activity(
97
+ session_id, project_path, "session_end",
98
+ final_summary or "Session ended"
99
+ )
100
+
101
+ result = await self.db.deregister_session(session_id)
102
+ return result
103
+
104
+ async def post_activity(
105
+ self, session_id: str, project_path: str,
106
+ event_type: str, summary: str, files: Optional[List[str]] = None
107
+ ) -> Dict[str, Any]:
108
+ """Post an activity event to the cross-session feed."""
109
+ return await self.db.post_session_activity(
110
+ session_id, project_path, event_type, summary, files
111
+ )
112
+
113
+ async def get_activity_feed(
114
+ self, project_path: str, limit: int = 20,
115
+ since: Optional[str] = None, exclude_session_id: Optional[str] = None
116
+ ) -> Dict[str, Any]:
117
+ """Get recent cross-session activity feed."""
118
+ events = await self.db.get_session_activity_feed(
119
+ project_path, limit, since, exclude_session_id
120
+ )
121
+ return {
122
+ "success": True,
123
+ "events": events,
124
+ "count": len(events),
125
+ }
126
+
127
+ async def get_catchup(
128
+ self, session_id: str, project_path: str,
129
+ since: Optional[str] = None
130
+ ) -> Dict[str, Any]:
131
+ """Get 'what happened while I was away' grouped by session."""
132
+ events = await self.db.get_session_activity_feed(
133
+ project_path, limit=50, since=since, exclude_session_id=session_id
134
+ )
135
+
136
+ # Group events by session
137
+ by_session: Dict[str, List[Dict[str, Any]]] = {}
138
+ for ev in events:
139
+ sid = ev["session_id"]
140
+ if sid not in by_session:
141
+ by_session[sid] = []
142
+ by_session[sid].append(ev)
143
+
144
+ # Get session labels/goals for context
145
+ siblings = await self.db.get_active_sessions(project_path)
146
+ session_info = {s["session_id"]: s for s in siblings}
147
+
148
+ grouped = []
149
+ for sid, session_events in by_session.items():
150
+ info = session_info.get(sid, {})
151
+ grouped.append({
152
+ "session_id": sid,
153
+ "session_label": info.get("session_label", ""),
154
+ "current_goal": info.get("current_goal", ""),
155
+ "status": info.get("status", "completed"),
156
+ "events": session_events,
157
+ "event_count": len(session_events),
158
+ })
159
+
160
+ return {
161
+ "success": True,
162
+ "sessions": grouped,
163
+ "total_events": len(events),
164
+ }
165
+
166
+ async def check_conflicts(
167
+ self, session_id: str, project_path: str
168
+ ) -> Dict[str, Any]:
169
+ """Check for file conflicts with sibling sessions."""
170
+ conflicts = await self.db.detect_file_conflicts(session_id, project_path)
171
+ return {
172
+ "success": True,
173
+ "conflicts": conflicts,
174
+ "has_conflicts": len(conflicts) > 0,
175
+ }
176
+
177
+ async def cleanup_stale(
178
+ self, idle_minutes: int = 10, completed_minutes: int = 30
179
+ ) -> Dict[str, Any]:
180
+ """Run stale session cleanup."""
181
+ return await self.db.cleanup_stale_sessions(idle_minutes, completed_minutes)