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.
- package/bin/cli.js +312 -136
- package/bin/postinstall.js +14 -0
- package/config.py +6 -0
- package/main.py +246 -1
- package/mcp_server.py +54 -0
- package/package.json +1 -1
- package/services/database.py +306 -0
- package/services/session_awareness.py +181 -0
|
@@ -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)
|