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.
Files changed (97) hide show
  1. package/README.md +206 -206
  2. package/agent_card.py +186 -0
  3. package/bin/cli.js +327 -185
  4. package/bin/lib/banner.js +39 -0
  5. package/bin/lib/environment.js +166 -0
  6. package/bin/lib/installer.js +291 -0
  7. package/bin/lib/models.js +95 -0
  8. package/bin/lib/steps/advanced.js +101 -0
  9. package/bin/lib/steps/confirm.js +87 -0
  10. package/bin/lib/steps/model.js +57 -0
  11. package/bin/lib/steps/provider.js +65 -0
  12. package/bin/lib/steps/scope.js +59 -0
  13. package/bin/lib/steps/server.js +74 -0
  14. package/bin/lib/ui.js +75 -0
  15. package/bin/onboarding.js +164 -0
  16. package/bin/postinstall.js +35 -270
  17. package/config.py +103 -4
  18. package/dashboard.html +4902 -2689
  19. package/hooks/extract_memories.py +439 -0
  20. package/hooks/grounding-hook.py +422 -348
  21. package/hooks/pre_compact_hook.py +76 -0
  22. package/hooks/session_end.py +293 -192
  23. package/hooks/session_end_hook.py +149 -0
  24. package/hooks/session_start.py +227 -227
  25. package/hooks/stop_hook.py +372 -0
  26. package/install.py +972 -902
  27. package/main.py +5240 -2859
  28. package/mcp_server.py +451 -0
  29. package/package.json +58 -47
  30. package/requirements.txt +12 -8
  31. package/services/__init__.py +50 -50
  32. package/services/adaptive_ranker.py +272 -0
  33. package/services/agent_catalog.json +153 -0
  34. package/services/agent_registry.py +245 -730
  35. package/services/claude_md_sync.py +320 -4
  36. package/services/consolidation.py +417 -0
  37. package/services/curator.py +1606 -0
  38. package/services/database.py +4118 -2485
  39. package/services/embedding_pipeline.py +262 -0
  40. package/services/embeddings.py +493 -85
  41. package/services/memory_decay.py +408 -0
  42. package/services/native_memory_paths.py +86 -0
  43. package/services/native_memory_sync.py +496 -0
  44. package/services/response_manager.py +183 -0
  45. package/services/terminal_ui.py +199 -0
  46. package/services/tier_manager.py +235 -0
  47. package/services/websocket.py +26 -6
  48. package/skills/__init__.py +21 -1
  49. package/skills/confidence_tracker.py +441 -0
  50. package/skills/context.py +675 -0
  51. package/skills/curator.py +348 -0
  52. package/skills/search.py +444 -213
  53. package/skills/session_review.py +605 -0
  54. package/skills/store.py +484 -179
  55. package/terminal_dashboard.py +474 -0
  56. package/update_system.py +829 -817
  57. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  58. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  59. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  60. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  61. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  63. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  64. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  65. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  66. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  67. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  68. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  69. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  70. package/services/__pycache__/database.cpython-312.pyc +0 -0
  71. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  72. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  74. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  75. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  76. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  77. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  78. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  81. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  82. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  83. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  84. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  85. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  86. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  87. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  88. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  89. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  90. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  91. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  92. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  93. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  94. package/test_automation.py +0 -221
  95. package/test_complete.py +0 -338
  96. package/test_full.py +0 -322
  97. 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)