claude-memory-agent 2.0.1 → 2.2.0
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/README.md +206 -206
- package/agent_card.py +186 -0
- package/bin/cli.js +327 -185
- package/bin/lib/banner.js +39 -0
- package/bin/lib/environment.js +166 -0
- package/bin/lib/installer.js +291 -0
- package/bin/lib/models.js +95 -0
- package/bin/lib/steps/advanced.js +101 -0
- package/bin/lib/steps/confirm.js +87 -0
- package/bin/lib/steps/model.js +57 -0
- package/bin/lib/steps/provider.js +65 -0
- package/bin/lib/steps/scope.js +59 -0
- package/bin/lib/steps/server.js +74 -0
- package/bin/lib/ui.js +75 -0
- package/bin/onboarding.js +164 -0
- package/bin/postinstall.js +35 -270
- package/config.py +103 -4
- package/dashboard.html +4902 -2689
- package/hooks/extract_memories.py +439 -0
- package/hooks/grounding-hook.py +422 -348
- package/hooks/pre_compact_hook.py +76 -0
- package/hooks/session_end.py +293 -192
- package/hooks/session_end_hook.py +149 -0
- package/hooks/session_start.py +227 -227
- package/hooks/stop_hook.py +372 -0
- package/install.py +972 -902
- package/main.py +5240 -2859
- package/mcp_server.py +451 -0
- package/package.json +58 -47
- package/requirements.txt +12 -8
- package/services/__init__.py +50 -50
- package/services/adaptive_ranker.py +272 -0
- package/services/agent_catalog.json +153 -0
- package/services/agent_registry.py +245 -730
- package/services/claude_md_sync.py +320 -4
- package/services/consolidation.py +417 -0
- package/services/curator.py +1606 -0
- package/services/database.py +4118 -2485
- package/services/embedding_pipeline.py +262 -0
- package/services/embeddings.py +493 -85
- package/services/memory_decay.py +408 -0
- package/services/native_memory_paths.py +86 -0
- package/services/native_memory_sync.py +496 -0
- package/services/response_manager.py +183 -0
- package/services/terminal_ui.py +199 -0
- package/services/tier_manager.py +235 -0
- package/services/websocket.py +26 -6
- package/skills/__init__.py +21 -1
- package/skills/confidence_tracker.py +441 -0
- package/skills/context.py +675 -0
- package/skills/curator.py +348 -0
- package/skills/search.py +444 -213
- package/skills/session_review.py +605 -0
- package/skills/store.py +484 -179
- package/terminal_dashboard.py +474 -0
- package/update_system.py +829 -817
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/test_automation.py +0 -221
- package/test_complete.py +0 -338
- package/test_full.py +0 -322
- package/verify_db.py +0 -134
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""Session review skill for end-of-session memory verification.
|
|
2
|
+
|
|
3
|
+
Allows users to review memories created during a session and mark them as:
|
|
4
|
+
- keep: Verified as useful, increases confidence
|
|
5
|
+
- discard: Not useful, decreases confidence significantly
|
|
6
|
+
- partial: Partially useful, sets confidence to middle value
|
|
7
|
+
"""
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from services.database import DatabaseService
|
|
10
|
+
from services.embeddings import EmbeddingService
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def _get_session_time_window(
|
|
14
|
+
db: DatabaseService,
|
|
15
|
+
session_id: str
|
|
16
|
+
) -> Optional[Dict[str, str]]:
|
|
17
|
+
"""
|
|
18
|
+
Look up a session's time window from the session_state table.
|
|
19
|
+
|
|
20
|
+
Returns dict with 'started_at' and 'ended_at' if found, None otherwise.
|
|
21
|
+
"""
|
|
22
|
+
session_row = await db.execute_query(
|
|
23
|
+
"""
|
|
24
|
+
SELECT created_at, updated_at, project_path, current_goal
|
|
25
|
+
FROM session_state
|
|
26
|
+
WHERE session_id = ?
|
|
27
|
+
AND session_id NOT LIKE '{%}'
|
|
28
|
+
LIMIT 1
|
|
29
|
+
""",
|
|
30
|
+
[session_id]
|
|
31
|
+
)
|
|
32
|
+
if session_row:
|
|
33
|
+
row = session_row[0]
|
|
34
|
+
return {
|
|
35
|
+
"started_at": row.get("created_at"),
|
|
36
|
+
"ended_at": row.get("updated_at") or row.get("created_at"),
|
|
37
|
+
"project_path": row.get("project_path"),
|
|
38
|
+
"current_goal": row.get("current_goal")
|
|
39
|
+
}
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def get_session_memories(
|
|
44
|
+
db: DatabaseService,
|
|
45
|
+
session_id: str,
|
|
46
|
+
include_patterns: bool = False,
|
|
47
|
+
limit: int = 100
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Get all memories created in a specific session for review.
|
|
51
|
+
|
|
52
|
+
Uses two strategies:
|
|
53
|
+
1. Direct match: memories WHERE session_id = ? (when memories have session_ids)
|
|
54
|
+
2. Time-window match: finds the session in session_state and matches memories
|
|
55
|
+
created within that session's time window (fallback for when memories
|
|
56
|
+
lack session_ids)
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
db: Database service instance
|
|
60
|
+
session_id: Session identifier to filter memories
|
|
61
|
+
include_patterns: Whether to include patterns created during session
|
|
62
|
+
limit: Maximum number of memories to return
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Dict with memories list and metadata
|
|
66
|
+
"""
|
|
67
|
+
if not session_id:
|
|
68
|
+
return {
|
|
69
|
+
"success": False,
|
|
70
|
+
"error": "session_id is required"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Strategy 1: Direct session_id match on memories table
|
|
74
|
+
memories_query = """
|
|
75
|
+
SELECT
|
|
76
|
+
id, type, content, project_path, project_name,
|
|
77
|
+
outcome, success, outcome_status, confidence,
|
|
78
|
+
importance, tags, created_at
|
|
79
|
+
FROM memories
|
|
80
|
+
WHERE session_id = ?
|
|
81
|
+
ORDER BY created_at DESC
|
|
82
|
+
LIMIT ?
|
|
83
|
+
"""
|
|
84
|
+
memories = await db.execute_query(memories_query, [session_id, limit])
|
|
85
|
+
|
|
86
|
+
# Strategy 2: Time-window fallback using session_state table
|
|
87
|
+
match_method = "session_id"
|
|
88
|
+
if not memories:
|
|
89
|
+
time_window = await _get_session_time_window(db, session_id)
|
|
90
|
+
if time_window:
|
|
91
|
+
tw_query = """
|
|
92
|
+
SELECT
|
|
93
|
+
id, type, content, project_path, project_name,
|
|
94
|
+
outcome, success, outcome_status, confidence,
|
|
95
|
+
importance, tags, created_at
|
|
96
|
+
FROM memories
|
|
97
|
+
WHERE created_at >= ?
|
|
98
|
+
AND created_at <= ?
|
|
99
|
+
"""
|
|
100
|
+
tw_params = [time_window["started_at"], time_window["ended_at"]]
|
|
101
|
+
|
|
102
|
+
# If the session has a project_path, filter by it
|
|
103
|
+
if time_window.get("project_path"):
|
|
104
|
+
tw_query += " AND project_path = ?"
|
|
105
|
+
tw_params.append(time_window["project_path"])
|
|
106
|
+
|
|
107
|
+
tw_query += " ORDER BY created_at DESC LIMIT ?"
|
|
108
|
+
tw_params.append(limit)
|
|
109
|
+
|
|
110
|
+
memories = await db.execute_query(tw_query, tw_params)
|
|
111
|
+
match_method = "time_window"
|
|
112
|
+
|
|
113
|
+
result = {
|
|
114
|
+
"success": True,
|
|
115
|
+
"session_id": session_id,
|
|
116
|
+
"memories": memories or [],
|
|
117
|
+
"memory_count": len(memories) if memories else 0,
|
|
118
|
+
"match_method": match_method
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Optionally include patterns
|
|
122
|
+
if include_patterns and memories:
|
|
123
|
+
min_time = min(m.get("created_at", "") for m in memories)
|
|
124
|
+
max_time = max(m.get("created_at", "") for m in memories)
|
|
125
|
+
patterns_query = """
|
|
126
|
+
SELECT
|
|
127
|
+
id, name, problem_type, solution,
|
|
128
|
+
success_count, failure_count, created_at
|
|
129
|
+
FROM patterns
|
|
130
|
+
WHERE created_at >= ?
|
|
131
|
+
AND created_at <= ?
|
|
132
|
+
ORDER BY created_at DESC
|
|
133
|
+
LIMIT ?
|
|
134
|
+
"""
|
|
135
|
+
patterns = await db.execute_query(patterns_query, [min_time, max_time, limit])
|
|
136
|
+
result["patterns"] = patterns or []
|
|
137
|
+
result["pattern_count"] = len(patterns) if patterns else 0
|
|
138
|
+
|
|
139
|
+
# Generate summary
|
|
140
|
+
type_counts = {}
|
|
141
|
+
for m in (memories or []):
|
|
142
|
+
mtype = m.get("type", "unknown")
|
|
143
|
+
type_counts[mtype] = type_counts.get(mtype, 0) + 1
|
|
144
|
+
|
|
145
|
+
result["summary"] = {
|
|
146
|
+
"by_type": type_counts,
|
|
147
|
+
"total_memories": result["memory_count"],
|
|
148
|
+
"avg_importance": (
|
|
149
|
+
sum(m.get("importance", 5) for m in (memories or [])) / len(memories)
|
|
150
|
+
if memories else 0
|
|
151
|
+
),
|
|
152
|
+
"avg_confidence": (
|
|
153
|
+
sum(m.get("confidence", 0.5) for m in (memories or [])) / len(memories)
|
|
154
|
+
if memories else 0
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def review_session_memories(
|
|
162
|
+
db: DatabaseService,
|
|
163
|
+
session_id: str,
|
|
164
|
+
reviews: List[Dict[str, Any]]
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
"""
|
|
167
|
+
Process user review decisions for session memories.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
db: Database service instance
|
|
171
|
+
session_id: Session identifier
|
|
172
|
+
reviews: List of review decisions, each containing:
|
|
173
|
+
- memory_id: ID of the memory
|
|
174
|
+
- decision: 'keep', 'discard', or 'partial'
|
|
175
|
+
- feedback: Optional user feedback
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Dict with processing results
|
|
179
|
+
"""
|
|
180
|
+
if not session_id:
|
|
181
|
+
return {
|
|
182
|
+
"success": False,
|
|
183
|
+
"error": "session_id is required"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if not reviews:
|
|
187
|
+
return {
|
|
188
|
+
"success": False,
|
|
189
|
+
"error": "reviews list is required"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
results = {
|
|
193
|
+
"success": True,
|
|
194
|
+
"session_id": session_id,
|
|
195
|
+
"processed": 0,
|
|
196
|
+
"kept": 0,
|
|
197
|
+
"discarded": 0,
|
|
198
|
+
"partial": 0,
|
|
199
|
+
"errors": []
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Confidence mappings for each decision
|
|
203
|
+
confidence_map = {
|
|
204
|
+
"keep": 0.9, # High confidence - verified useful
|
|
205
|
+
"partial": 0.5, # Medium confidence - partially useful
|
|
206
|
+
"discard": 0.1 # Low confidence - not useful
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
outcome_status_map = {
|
|
210
|
+
"keep": "success",
|
|
211
|
+
"partial": "partial",
|
|
212
|
+
"discard": "failed"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for review in reviews:
|
|
216
|
+
memory_id = review.get("memory_id")
|
|
217
|
+
decision = review.get("decision", "keep").lower()
|
|
218
|
+
feedback = review.get("feedback")
|
|
219
|
+
|
|
220
|
+
if not memory_id:
|
|
221
|
+
results["errors"].append({
|
|
222
|
+
"error": "memory_id is required",
|
|
223
|
+
"review": review
|
|
224
|
+
})
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if decision not in confidence_map:
|
|
228
|
+
results["errors"].append({
|
|
229
|
+
"memory_id": memory_id,
|
|
230
|
+
"error": f"Invalid decision: {decision}. Must be 'keep', 'discard', or 'partial'"
|
|
231
|
+
})
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Update confidence
|
|
236
|
+
new_confidence = confidence_map[decision]
|
|
237
|
+
await db.update_memory_confidence(memory_id, new_confidence)
|
|
238
|
+
|
|
239
|
+
# Update outcome status
|
|
240
|
+
new_outcome_status = outcome_status_map[decision]
|
|
241
|
+
await db.update_memory_outcome(
|
|
242
|
+
memory_id=memory_id,
|
|
243
|
+
outcome_status=new_outcome_status
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Store feedback if provided
|
|
247
|
+
if feedback:
|
|
248
|
+
await db.execute_query(
|
|
249
|
+
"UPDATE memories SET user_feedback = ? WHERE id = ?",
|
|
250
|
+
[feedback, memory_id]
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
results["processed"] += 1
|
|
254
|
+
|
|
255
|
+
if decision == "keep":
|
|
256
|
+
results["kept"] += 1
|
|
257
|
+
elif decision == "discard":
|
|
258
|
+
results["discarded"] += 1
|
|
259
|
+
elif decision == "partial":
|
|
260
|
+
results["partial"] += 1
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
results["errors"].append({
|
|
264
|
+
"memory_id": memory_id,
|
|
265
|
+
"error": str(e)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
# Calculate success rate
|
|
269
|
+
total = results["kept"] + results["discarded"] + results["partial"]
|
|
270
|
+
if total > 0:
|
|
271
|
+
results["keep_rate"] = round(results["kept"] / total * 100, 1)
|
|
272
|
+
else:
|
|
273
|
+
results["keep_rate"] = 0
|
|
274
|
+
|
|
275
|
+
return results
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def suggest_session_reviews(
|
|
279
|
+
db: DatabaseService,
|
|
280
|
+
embeddings: EmbeddingService,
|
|
281
|
+
session_id: str
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Analyze session memories and suggest which to keep/discard.
|
|
285
|
+
|
|
286
|
+
Suggestions are based on:
|
|
287
|
+
- Memory type (decisions and patterns more likely to keep)
|
|
288
|
+
- Importance score
|
|
289
|
+
- Outcome status if already set
|
|
290
|
+
- Similarity to existing successful memories
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
db: Database service instance
|
|
294
|
+
embeddings: Embedding service instance
|
|
295
|
+
session_id: Session identifier
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dict with suggested reviews
|
|
299
|
+
"""
|
|
300
|
+
if not session_id:
|
|
301
|
+
return {
|
|
302
|
+
"success": False,
|
|
303
|
+
"error": "session_id is required"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Get session memories
|
|
307
|
+
memories_result = await get_session_memories(db, session_id)
|
|
308
|
+
if not memories_result.get("success"):
|
|
309
|
+
return memories_result
|
|
310
|
+
|
|
311
|
+
memories = memories_result.get("memories", [])
|
|
312
|
+
suggestions = []
|
|
313
|
+
|
|
314
|
+
# Types that are more likely to be valuable
|
|
315
|
+
high_value_types = {"decision", "error", "pattern", "preference"}
|
|
316
|
+
|
|
317
|
+
for memory in memories:
|
|
318
|
+
memory_id = memory.get("id")
|
|
319
|
+
memory_type = memory.get("type", "chunk")
|
|
320
|
+
importance = memory.get("importance", 5)
|
|
321
|
+
outcome_status = memory.get("outcome_status", "pending")
|
|
322
|
+
confidence = memory.get("confidence", 0.5)
|
|
323
|
+
content = memory.get("content", "")
|
|
324
|
+
|
|
325
|
+
suggestion = {
|
|
326
|
+
"memory_id": memory_id,
|
|
327
|
+
"type": memory_type,
|
|
328
|
+
"content_preview": content[:200] + "..." if len(content) > 200 else content,
|
|
329
|
+
"current_confidence": confidence,
|
|
330
|
+
"importance": importance
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Determine suggestion based on heuristics
|
|
334
|
+
if outcome_status == "success":
|
|
335
|
+
suggestion["suggested_decision"] = "keep"
|
|
336
|
+
suggestion["reason"] = "Already marked as successful"
|
|
337
|
+
elif outcome_status == "failed":
|
|
338
|
+
suggestion["suggested_decision"] = "discard"
|
|
339
|
+
suggestion["reason"] = "Already marked as failed"
|
|
340
|
+
elif outcome_status == "partial":
|
|
341
|
+
suggestion["suggested_decision"] = "partial"
|
|
342
|
+
suggestion["reason"] = "Already marked as partial success"
|
|
343
|
+
elif memory_type in high_value_types:
|
|
344
|
+
if importance >= 7:
|
|
345
|
+
suggestion["suggested_decision"] = "keep"
|
|
346
|
+
suggestion["reason"] = f"High importance {memory_type}"
|
|
347
|
+
else:
|
|
348
|
+
suggestion["suggested_decision"] = "partial"
|
|
349
|
+
suggestion["reason"] = f"Review this {memory_type}"
|
|
350
|
+
elif importance >= 8:
|
|
351
|
+
suggestion["suggested_decision"] = "keep"
|
|
352
|
+
suggestion["reason"] = "High importance score"
|
|
353
|
+
elif importance <= 3:
|
|
354
|
+
suggestion["suggested_decision"] = "discard"
|
|
355
|
+
suggestion["reason"] = "Low importance score"
|
|
356
|
+
else:
|
|
357
|
+
suggestion["suggested_decision"] = "partial"
|
|
358
|
+
suggestion["reason"] = "Review recommended"
|
|
359
|
+
|
|
360
|
+
suggestions.append(suggestion)
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"session_id": session_id,
|
|
365
|
+
"suggestions": suggestions,
|
|
366
|
+
"summary": {
|
|
367
|
+
"total": len(suggestions),
|
|
368
|
+
"suggested_keep": sum(1 for s in suggestions if s["suggested_decision"] == "keep"),
|
|
369
|
+
"suggested_discard": sum(1 for s in suggestions if s["suggested_decision"] == "discard"),
|
|
370
|
+
"suggested_partial": sum(1 for s in suggestions if s["suggested_decision"] == "partial")
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def get_recent_sessions(
|
|
376
|
+
db: DatabaseService,
|
|
377
|
+
project_path: Optional[str] = None,
|
|
378
|
+
limit: int = 10
|
|
379
|
+
) -> Dict[str, Any]:
|
|
380
|
+
"""
|
|
381
|
+
Get recent sessions with memory counts for review selection.
|
|
382
|
+
|
|
383
|
+
Uses two strategies and merges results:
|
|
384
|
+
1. Sessions from session_state table (with memory counts via time-window matching)
|
|
385
|
+
2. Sessions derived from memories table (when memories have session_ids)
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
db: Database service instance
|
|
389
|
+
project_path: Optional filter by project
|
|
390
|
+
limit: Maximum number of sessions to return
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Dict with session list
|
|
394
|
+
"""
|
|
395
|
+
seen_session_ids = set()
|
|
396
|
+
all_sessions = []
|
|
397
|
+
|
|
398
|
+
# Strategy 1: Get sessions from session_state table
|
|
399
|
+
# Filter out JSON blob rows (session_id starts with '{') which are state dumps
|
|
400
|
+
state_query = """
|
|
401
|
+
SELECT
|
|
402
|
+
s.session_id,
|
|
403
|
+
s.project_path,
|
|
404
|
+
s.current_goal,
|
|
405
|
+
s.created_at as started_at,
|
|
406
|
+
s.updated_at as ended_at
|
|
407
|
+
FROM session_state s
|
|
408
|
+
WHERE s.session_id NOT LIKE '{%}'
|
|
409
|
+
"""
|
|
410
|
+
state_params = []
|
|
411
|
+
|
|
412
|
+
if project_path:
|
|
413
|
+
state_query += " AND s.project_path = ?"
|
|
414
|
+
state_params.append(project_path)
|
|
415
|
+
|
|
416
|
+
state_query += """
|
|
417
|
+
ORDER BY COALESCE(s.updated_at, s.created_at) DESC
|
|
418
|
+
LIMIT ?
|
|
419
|
+
"""
|
|
420
|
+
state_params.append(limit * 2) # Fetch extra to account for filtering
|
|
421
|
+
|
|
422
|
+
state_sessions = await db.execute_query(state_query, state_params)
|
|
423
|
+
|
|
424
|
+
for ss in (state_sessions or []):
|
|
425
|
+
sid = ss.get("session_id")
|
|
426
|
+
if not sid or sid in seen_session_ids:
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
started_at = ss.get("started_at")
|
|
430
|
+
ended_at = ss.get("ended_at") or started_at
|
|
431
|
+
sess_project = ss.get("project_path")
|
|
432
|
+
|
|
433
|
+
# Count memories in this session's time window
|
|
434
|
+
count_query = """
|
|
435
|
+
SELECT
|
|
436
|
+
COUNT(*) as memory_count,
|
|
437
|
+
SUM(CASE WHEN outcome_status = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
438
|
+
SUM(CASE WHEN outcome_status = 'failed' THEN 1 ELSE 0 END) as failed_count,
|
|
439
|
+
SUM(CASE WHEN outcome_status = 'pending' OR outcome_status IS NULL THEN 1 ELSE 0 END) as pending_count,
|
|
440
|
+
AVG(confidence) as avg_confidence,
|
|
441
|
+
MAX(project_path) as project_path,
|
|
442
|
+
MAX(project_name) as project_name
|
|
443
|
+
FROM memories
|
|
444
|
+
WHERE created_at >= ?
|
|
445
|
+
AND created_at <= ?
|
|
446
|
+
"""
|
|
447
|
+
count_params = [started_at, ended_at]
|
|
448
|
+
|
|
449
|
+
if sess_project:
|
|
450
|
+
count_query += " AND project_path = ?"
|
|
451
|
+
count_params.append(sess_project)
|
|
452
|
+
|
|
453
|
+
counts = await db.execute_query(count_query, count_params)
|
|
454
|
+
count_row = counts[0] if counts else {}
|
|
455
|
+
|
|
456
|
+
memory_count = count_row.get("memory_count", 0) or 0
|
|
457
|
+
|
|
458
|
+
# Also check if any memories reference this session_id directly
|
|
459
|
+
direct_count_result = await db.execute_query(
|
|
460
|
+
"SELECT COUNT(*) as cnt FROM memories WHERE session_id = ?",
|
|
461
|
+
[sid]
|
|
462
|
+
)
|
|
463
|
+
direct_count = (direct_count_result[0].get("cnt", 0) if direct_count_result else 0) or 0
|
|
464
|
+
memory_count = max(memory_count, direct_count)
|
|
465
|
+
|
|
466
|
+
session_entry = {
|
|
467
|
+
"session_id": sid,
|
|
468
|
+
"memory_count": memory_count,
|
|
469
|
+
"started_at": started_at,
|
|
470
|
+
"ended_at": ended_at,
|
|
471
|
+
"project_path": count_row.get("project_path") or sess_project,
|
|
472
|
+
"project_name": count_row.get("project_name"),
|
|
473
|
+
"current_goal": ss.get("current_goal"),
|
|
474
|
+
"success_count": count_row.get("success_count", 0) or 0,
|
|
475
|
+
"failed_count": count_row.get("failed_count", 0) or 0,
|
|
476
|
+
"pending_count": count_row.get("pending_count", 0) or 0,
|
|
477
|
+
"avg_confidence": count_row.get("avg_confidence", 0.5) or 0.5,
|
|
478
|
+
"source": "session_state"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
all_sessions.append(session_entry)
|
|
482
|
+
seen_session_ids.add(sid)
|
|
483
|
+
|
|
484
|
+
# Strategy 2: Also get sessions derived from memories table
|
|
485
|
+
# (for when memories have explicit session_ids not in session_state)
|
|
486
|
+
mem_query = """
|
|
487
|
+
SELECT
|
|
488
|
+
session_id,
|
|
489
|
+
COUNT(*) as memory_count,
|
|
490
|
+
MIN(created_at) as started_at,
|
|
491
|
+
MAX(created_at) as ended_at,
|
|
492
|
+
MAX(project_path) as project_path,
|
|
493
|
+
MAX(project_name) as project_name,
|
|
494
|
+
SUM(CASE WHEN outcome_status = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
495
|
+
SUM(CASE WHEN outcome_status = 'failed' THEN 1 ELSE 0 END) as failed_count,
|
|
496
|
+
SUM(CASE WHEN outcome_status = 'pending' OR outcome_status IS NULL THEN 1 ELSE 0 END) as pending_count,
|
|
497
|
+
AVG(confidence) as avg_confidence
|
|
498
|
+
FROM memories
|
|
499
|
+
WHERE session_id IS NOT NULL
|
|
500
|
+
AND session_id != ''
|
|
501
|
+
"""
|
|
502
|
+
mem_params = []
|
|
503
|
+
|
|
504
|
+
if project_path:
|
|
505
|
+
mem_query += " AND project_path = ?"
|
|
506
|
+
mem_params.append(project_path)
|
|
507
|
+
|
|
508
|
+
mem_query += """
|
|
509
|
+
GROUP BY session_id
|
|
510
|
+
ORDER BY MAX(created_at) DESC
|
|
511
|
+
LIMIT ?
|
|
512
|
+
"""
|
|
513
|
+
mem_params.append(limit)
|
|
514
|
+
|
|
515
|
+
mem_sessions = await db.execute_query(mem_query, mem_params)
|
|
516
|
+
|
|
517
|
+
for ms in (mem_sessions or []):
|
|
518
|
+
sid = ms.get("session_id")
|
|
519
|
+
if not sid or sid in seen_session_ids:
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
session_entry = {
|
|
523
|
+
"session_id": sid,
|
|
524
|
+
"memory_count": ms.get("memory_count", 0) or 0,
|
|
525
|
+
"started_at": ms.get("started_at"),
|
|
526
|
+
"ended_at": ms.get("ended_at"),
|
|
527
|
+
"project_path": ms.get("project_path"),
|
|
528
|
+
"project_name": ms.get("project_name"),
|
|
529
|
+
"current_goal": None,
|
|
530
|
+
"success_count": ms.get("success_count", 0) or 0,
|
|
531
|
+
"failed_count": ms.get("failed_count", 0) or 0,
|
|
532
|
+
"pending_count": ms.get("pending_count", 0) or 0,
|
|
533
|
+
"avg_confidence": ms.get("avg_confidence", 0.5) or 0.5,
|
|
534
|
+
"source": "memories"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
all_sessions.append(session_entry)
|
|
538
|
+
seen_session_ids.add(sid)
|
|
539
|
+
|
|
540
|
+
# Sort by most recent activity and apply limit
|
|
541
|
+
all_sessions.sort(
|
|
542
|
+
key=lambda s: s.get("ended_at") or s.get("started_at") or "",
|
|
543
|
+
reverse=True
|
|
544
|
+
)
|
|
545
|
+
all_sessions = all_sessions[:limit]
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
"success": True,
|
|
549
|
+
"sessions": all_sessions,
|
|
550
|
+
"count": len(all_sessions)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
async def bulk_review_by_type(
|
|
555
|
+
db: DatabaseService,
|
|
556
|
+
session_id: str,
|
|
557
|
+
type_decisions: Dict[str, str]
|
|
558
|
+
) -> Dict[str, Any]:
|
|
559
|
+
"""
|
|
560
|
+
Apply review decisions to all memories of specific types in a session.
|
|
561
|
+
|
|
562
|
+
Useful for quickly processing sessions, e.g.:
|
|
563
|
+
- Keep all 'decision' and 'error' memories
|
|
564
|
+
- Discard all 'chunk' memories
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
db: Database service instance
|
|
568
|
+
session_id: Session identifier
|
|
569
|
+
type_decisions: Dict mapping memory types to decisions
|
|
570
|
+
e.g., {"decision": "keep", "chunk": "discard", "error": "keep"}
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Dict with processing results
|
|
574
|
+
"""
|
|
575
|
+
if not session_id:
|
|
576
|
+
return {
|
|
577
|
+
"success": False,
|
|
578
|
+
"error": "session_id is required"
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if not type_decisions:
|
|
582
|
+
return {
|
|
583
|
+
"success": False,
|
|
584
|
+
"error": "type_decisions is required"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
# Get session memories
|
|
588
|
+
memories_result = await get_session_memories(db, session_id)
|
|
589
|
+
if not memories_result.get("success"):
|
|
590
|
+
return memories_result
|
|
591
|
+
|
|
592
|
+
memories = memories_result.get("memories", [])
|
|
593
|
+
|
|
594
|
+
# Build reviews based on type decisions
|
|
595
|
+
reviews = []
|
|
596
|
+
for memory in memories:
|
|
597
|
+
memory_type = memory.get("type", "chunk")
|
|
598
|
+
if memory_type in type_decisions:
|
|
599
|
+
reviews.append({
|
|
600
|
+
"memory_id": memory.get("id"),
|
|
601
|
+
"decision": type_decisions[memory_type]
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
# Process reviews
|
|
605
|
+
return await review_session_memories(db, session_id, reviews)
|