claude-memory-agent 2.1.0 → 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/bin/cli.js +11 -1
- 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 +22 -257
- package/config.py +103 -4
- package/dashboard.html +697 -27
- package/hooks/extract_memories.py +439 -0
- package/hooks/pre_compact_hook.py +76 -0
- package/hooks/session_end_hook.py +149 -0
- package/hooks/stop_hook.py +372 -0
- package/install.py +85 -32
- package/main.py +1636 -892
- package/mcp_server.py +451 -0
- package/package.json +14 -3
- package/requirements.txt +12 -8
- 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/database.py +586 -105
- 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/search.py +136 -61
- package/skills/session_review.py +210 -23
- package/skills/store.py +125 -18
- package/terminal_dashboard.py +474 -0
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/grounding-hook.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__/curator.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__/confidence_tracker.cpython-312.pyc +0 -0
- package/skills/__pycache__/context.cpython-312.pyc +0 -0
- package/skills/__pycache__/curator.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__/session_review.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
package/skills/search.py
CHANGED
|
@@ -7,85 +7,132 @@ from services.embeddings import EmbeddingService
|
|
|
7
7
|
logger = logging.getLogger(__name__)
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
async def _enrich_with_graph_context(db: DatabaseService, results: list) -> list:
|
|
10
|
+
async def _enrich_with_graph_context(db: DatabaseService, results: list, max_related_items: int = 2) -> list:
|
|
11
11
|
"""
|
|
12
|
-
Enrich search results with relationship context.
|
|
13
|
-
|
|
12
|
+
Enrich search results with relationship context using batch queries.
|
|
13
|
+
|
|
14
|
+
Uses batch DB queries (IN clauses) instead of per-result sequential queries,
|
|
15
|
+
providing 5-10x speedup on the enrichment phase for typical 10-result sets.
|
|
14
16
|
|
|
15
17
|
Args:
|
|
16
18
|
db: Database service instance
|
|
17
19
|
results: List of search results to enrich
|
|
20
|
+
max_related_items: Max related items per relationship type (caps response size)
|
|
18
21
|
|
|
19
22
|
Returns:
|
|
20
23
|
List of enriched results with graph context added
|
|
21
24
|
"""
|
|
25
|
+
import asyncio
|
|
26
|
+
|
|
27
|
+
if not results:
|
|
28
|
+
return results
|
|
29
|
+
|
|
30
|
+
# Collect all memory IDs that need enrichment
|
|
31
|
+
all_ids = [r.get('id') for r in results if r.get('id')]
|
|
32
|
+
if not all_ids:
|
|
33
|
+
return results
|
|
34
|
+
|
|
35
|
+
# Categorize IDs by type for targeted batch queries
|
|
36
|
+
error_ids = [r['id'] for r in results if r.get('id') and r.get('type') == 'error']
|
|
37
|
+
decision_ids = [r['id'] for r in results if r.get('id') and r.get('type') == 'decision']
|
|
38
|
+
causal_ids = [r['id'] for r in results if r.get('id') and r.get('type') in ('error', 'decision', 'code')]
|
|
39
|
+
|
|
40
|
+
# Run all batch queries in parallel
|
|
41
|
+
batch_tasks = {}
|
|
42
|
+
|
|
43
|
+
# Batch: fixes for errors (incoming 'fixes' relationships)
|
|
44
|
+
if error_ids:
|
|
45
|
+
batch_tasks['fixes'] = db.get_related_memories_batch(error_ids, 'fixes', direction='incoming')
|
|
46
|
+
|
|
47
|
+
# Batch: supports for decisions (incoming 'supports' relationships)
|
|
48
|
+
if decision_ids:
|
|
49
|
+
batch_tasks['supports'] = db.get_related_memories_batch(decision_ids, 'supports', direction='incoming')
|
|
50
|
+
batch_tasks['caused'] = db.get_related_memories_batch(decision_ids, 'caused_by', direction='outgoing')
|
|
51
|
+
|
|
52
|
+
# Batch: contradictions for all results
|
|
53
|
+
batch_tasks['contradictions'] = db.find_contradictions_batch(all_ids)
|
|
54
|
+
|
|
55
|
+
# Causal chains still need per-ID traversal (recursive), but run in parallel
|
|
56
|
+
causal_futures = {}
|
|
57
|
+
for mid in causal_ids:
|
|
58
|
+
causal_futures[mid] = db.get_causal_chain(mid, max_depth=3)
|
|
59
|
+
|
|
60
|
+
# Gather all batch results
|
|
61
|
+
batch_results = {}
|
|
62
|
+
try:
|
|
63
|
+
if batch_tasks:
|
|
64
|
+
keys = list(batch_tasks.keys())
|
|
65
|
+
values = await asyncio.gather(*batch_tasks.values(), return_exceptions=True)
|
|
66
|
+
for k, v in zip(keys, values):
|
|
67
|
+
batch_results[k] = v if not isinstance(v, Exception) else {}
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.warning(f"Batch graph enrichment failed: {e}")
|
|
70
|
+
batch_results = {}
|
|
71
|
+
|
|
72
|
+
# Gather causal chain results in parallel
|
|
73
|
+
causal_results = {}
|
|
74
|
+
if causal_futures:
|
|
75
|
+
try:
|
|
76
|
+
keys = list(causal_futures.keys())
|
|
77
|
+
values = await asyncio.gather(*causal_futures.values(), return_exceptions=True)
|
|
78
|
+
for k, v in zip(keys, values):
|
|
79
|
+
causal_results[k] = v if not isinstance(v, Exception) else None
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.debug(f"Causal chain batch failed: {e}")
|
|
82
|
+
|
|
83
|
+
# Assemble enriched results
|
|
22
84
|
enriched = []
|
|
85
|
+
fixes_map = batch_results.get('fixes', {})
|
|
86
|
+
supports_map = batch_results.get('supports', {})
|
|
87
|
+
caused_map = batch_results.get('caused', {})
|
|
88
|
+
contradictions_map = batch_results.get('contradictions', {})
|
|
89
|
+
|
|
23
90
|
for result in results:
|
|
24
91
|
memory_id = result.get('id')
|
|
25
92
|
if not memory_id:
|
|
26
93
|
enriched.append(result)
|
|
27
94
|
continue
|
|
28
95
|
|
|
29
|
-
# Create enriched copy
|
|
30
96
|
enriched_result = dict(result)
|
|
31
|
-
|
|
32
97
|
memory_type = result.get('type', '')
|
|
33
98
|
|
|
34
|
-
#
|
|
35
|
-
if memory_type == 'error':
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
logger.debug(f"Failed to get fixes for memory {memory_id}: {e}")
|
|
45
|
-
|
|
46
|
-
# For decisions: find rationale and consequences
|
|
99
|
+
# Errors: known fixes
|
|
100
|
+
if memory_type == 'error' and memory_id in fixes_map:
|
|
101
|
+
fixes = fixes_map[memory_id]
|
|
102
|
+
if fixes:
|
|
103
|
+
enriched_result['known_fixes'] = [
|
|
104
|
+
{'id': f['id'], 'content': f['content'][:200], 'outcome': f.get('outcome')}
|
|
105
|
+
for f in fixes[:max_related_items]
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# Decisions: rationale and consequences
|
|
47
109
|
if memory_type == 'decision':
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
logger.debug(f"Failed to get supports for memory {memory_id}: {e}")
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
# What this decision caused
|
|
61
|
-
caused = await db.get_related_memories(memory_id, 'caused_by', direction='outgoing', depth=1)
|
|
62
|
-
if caused:
|
|
63
|
-
enriched_result['consequences'] = [
|
|
64
|
-
{'id': c['id'], 'content': c['content'][:200]}
|
|
65
|
-
for c in caused
|
|
66
|
-
]
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logger.debug(f"Failed to get consequences for memory {memory_id}: {e}")
|
|
69
|
-
|
|
70
|
-
# For all types: find contradictions (critical for anti-hallucination)
|
|
71
|
-
try:
|
|
72
|
-
contradictions = await db.find_contradictions(memory_id)
|
|
73
|
-
if contradictions:
|
|
74
|
-
enriched_result['contradictions'] = [
|
|
110
|
+
supports = supports_map.get(memory_id, [])
|
|
111
|
+
if supports:
|
|
112
|
+
enriched_result['rationale'] = [
|
|
113
|
+
{'id': s['id'], 'content': s['content'][:200]}
|
|
114
|
+
for s in supports[:max_related_items]
|
|
115
|
+
]
|
|
116
|
+
caused = caused_map.get(memory_id, [])
|
|
117
|
+
if caused:
|
|
118
|
+
enriched_result['consequences'] = [
|
|
75
119
|
{'id': c['id'], 'content': c['content'][:200]}
|
|
76
|
-
for c in
|
|
120
|
+
for c in caused[:max_related_items]
|
|
77
121
|
]
|
|
78
|
-
except Exception as e:
|
|
79
|
-
logger.debug(f"Failed to get contradictions for memory {memory_id}: {e}")
|
|
80
122
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
123
|
+
# All types: contradictions
|
|
124
|
+
contradictions = contradictions_map.get(memory_id, [])
|
|
125
|
+
if contradictions:
|
|
126
|
+
enriched_result['contradictions'] = [
|
|
127
|
+
{'id': c['id'], 'content': c['content'][:200]}
|
|
128
|
+
for c in contradictions[:max_related_items]
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
# Causal chains
|
|
132
|
+
if memory_type in ('error', 'decision', 'code') and memory_id in causal_results:
|
|
133
|
+
chain = causal_results[memory_id]
|
|
134
|
+
if chain and (chain.get('causes') or chain.get('fixes') or chain.get('root_causes')):
|
|
135
|
+
enriched_result['causal_chain'] = chain
|
|
89
136
|
|
|
90
137
|
enriched.append(enriched_result)
|
|
91
138
|
|
|
@@ -112,7 +159,9 @@ async def semantic_search(
|
|
|
112
159
|
current_context: Optional[Dict[str, Any]] = None,
|
|
113
160
|
auto_detect_context: bool = True,
|
|
114
161
|
# Graph enrichment
|
|
115
|
-
include_graph: bool = True
|
|
162
|
+
include_graph: bool = True,
|
|
163
|
+
# Adaptive ranking
|
|
164
|
+
temperature: Optional[float] = None
|
|
116
165
|
) -> Dict[str, Any]:
|
|
117
166
|
"""
|
|
118
167
|
Search memories using semantic similarity with context filters.
|
|
@@ -190,7 +239,9 @@ async def semantic_search(
|
|
|
190
239
|
include_superseded=include_superseded,
|
|
191
240
|
include_unreliable=include_unreliable,
|
|
192
241
|
outcome_status=outcome_status,
|
|
193
|
-
current_context=search_context
|
|
242
|
+
current_context=search_context,
|
|
243
|
+
query_text=query,
|
|
244
|
+
temperature=temperature
|
|
194
245
|
)
|
|
195
246
|
else:
|
|
196
247
|
# Fallback to keyword search
|
|
@@ -217,6 +268,18 @@ async def semantic_search(
|
|
|
217
268
|
logger.warning(f"Failed to enrich with graph context: {e}")
|
|
218
269
|
# Continue with unenriched results
|
|
219
270
|
|
|
271
|
+
# Promote accessed memories if their tier should be higher (fire-and-forget)
|
|
272
|
+
if results:
|
|
273
|
+
try:
|
|
274
|
+
from services.tier_manager import TierManager
|
|
275
|
+
tier_mgr = TierManager(db)
|
|
276
|
+
for r in results:
|
|
277
|
+
mem_id = r.get('id')
|
|
278
|
+
if mem_id:
|
|
279
|
+
await tier_mgr.promote_on_access(mem_id)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.debug(f"Tier promotion on access failed (non-fatal): {e}")
|
|
282
|
+
|
|
220
283
|
return {
|
|
221
284
|
"success": True,
|
|
222
285
|
"query": query,
|
|
@@ -237,7 +300,8 @@ async def semantic_search(
|
|
|
237
300
|
},
|
|
238
301
|
"context_aware": search_context is not None,
|
|
239
302
|
"detected_context": detected_context,
|
|
240
|
-
"threshold": threshold if search_method == "semantic" else None
|
|
303
|
+
"threshold": threshold if search_method == "semantic" else None,
|
|
304
|
+
"temperature": temperature
|
|
241
305
|
}
|
|
242
306
|
|
|
243
307
|
|
|
@@ -302,7 +366,7 @@ async def get_project_context(
|
|
|
302
366
|
embeddings: EmbeddingService,
|
|
303
367
|
project_path: str,
|
|
304
368
|
query: Optional[str] = None,
|
|
305
|
-
limit: int =
|
|
369
|
+
limit: int = 5
|
|
306
370
|
) -> Dict[str, Any]:
|
|
307
371
|
"""
|
|
308
372
|
Get all relevant context for a project.
|
|
@@ -314,7 +378,7 @@ async def get_project_context(
|
|
|
314
378
|
embeddings: Embedding service instance
|
|
315
379
|
project_path: Path to the project
|
|
316
380
|
query: Optional query to filter relevant memories
|
|
317
|
-
limit: Max memories to return
|
|
381
|
+
limit: Max memories to return (default reduced to 5 for response size)
|
|
318
382
|
|
|
319
383
|
Returns:
|
|
320
384
|
Dict with project info and relevant memories
|
|
@@ -358,6 +422,17 @@ async def get_project_context(
|
|
|
358
422
|
limit=limit
|
|
359
423
|
)
|
|
360
424
|
|
|
425
|
+
# Enrich all result lists with graph context (relationships, contradictions, causal chains)
|
|
426
|
+
try:
|
|
427
|
+
if decisions:
|
|
428
|
+
decisions = await _enrich_with_graph_context(db, decisions)
|
|
429
|
+
if patterns:
|
|
430
|
+
patterns = await _enrich_with_graph_context(db, patterns)
|
|
431
|
+
if relevant:
|
|
432
|
+
relevant = await _enrich_with_graph_context(db, relevant)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
logger.warning(f"Failed to enrich project context with graph: {e}")
|
|
435
|
+
|
|
361
436
|
return {
|
|
362
437
|
"success": True,
|
|
363
438
|
"project": project,
|
package/skills/session_review.py
CHANGED
|
@@ -10,6 +10,36 @@ from services.database import DatabaseService
|
|
|
10
10
|
from services.embeddings import EmbeddingService
|
|
11
11
|
|
|
12
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
|
+
|
|
13
43
|
async def get_session_memories(
|
|
14
44
|
db: DatabaseService,
|
|
15
45
|
session_id: str,
|
|
@@ -19,6 +49,12 @@ async def get_session_memories(
|
|
|
19
49
|
"""
|
|
20
50
|
Get all memories created in a specific session for review.
|
|
21
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
|
+
|
|
22
58
|
Args:
|
|
23
59
|
db: Database service instance
|
|
24
60
|
session_id: Session identifier to filter memories
|
|
@@ -34,7 +70,7 @@ async def get_session_memories(
|
|
|
34
70
|
"error": "session_id is required"
|
|
35
71
|
}
|
|
36
72
|
|
|
37
|
-
#
|
|
73
|
+
# Strategy 1: Direct session_id match on memories table
|
|
38
74
|
memories_query = """
|
|
39
75
|
SELECT
|
|
40
76
|
id, type, content, project_path, project_name,
|
|
@@ -45,33 +81,58 @@ async def get_session_memories(
|
|
|
45
81
|
ORDER BY created_at DESC
|
|
46
82
|
LIMIT ?
|
|
47
83
|
"""
|
|
48
|
-
|
|
49
84
|
memories = await db.execute_query(memories_query, [session_id, limit])
|
|
50
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
|
+
|
|
51
113
|
result = {
|
|
52
114
|
"success": True,
|
|
53
115
|
"session_id": session_id,
|
|
54
116
|
"memories": memories or [],
|
|
55
|
-
"memory_count": len(memories) if memories else 0
|
|
117
|
+
"memory_count": len(memories) if memories else 0,
|
|
118
|
+
"match_method": match_method
|
|
56
119
|
}
|
|
57
120
|
|
|
58
121
|
# Optionally include patterns
|
|
59
|
-
if 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)
|
|
60
125
|
patterns_query = """
|
|
61
126
|
SELECT
|
|
62
127
|
id, name, problem_type, solution,
|
|
63
128
|
success_count, failure_count, created_at
|
|
64
129
|
FROM patterns
|
|
65
|
-
WHERE created_at >=
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
AND created_at <= (
|
|
69
|
-
SELECT MAX(created_at) FROM memories WHERE session_id = ?
|
|
70
|
-
)
|
|
130
|
+
WHERE created_at >= ?
|
|
131
|
+
AND created_at <= ?
|
|
71
132
|
ORDER BY created_at DESC
|
|
72
133
|
LIMIT ?
|
|
73
134
|
"""
|
|
74
|
-
patterns = await db.execute_query(patterns_query, [
|
|
135
|
+
patterns = await db.execute_query(patterns_query, [min_time, max_time, limit])
|
|
75
136
|
result["patterns"] = patterns or []
|
|
76
137
|
result["pattern_count"] = len(patterns) if patterns else 0
|
|
77
138
|
|
|
@@ -319,6 +380,10 @@ async def get_recent_sessions(
|
|
|
319
380
|
"""
|
|
320
381
|
Get recent sessions with memory counts for review selection.
|
|
321
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
|
+
|
|
322
387
|
Args:
|
|
323
388
|
db: Database service instance
|
|
324
389
|
project_path: Optional filter by project
|
|
@@ -327,40 +392,162 @@ async def get_recent_sessions(
|
|
|
327
392
|
Returns:
|
|
328
393
|
Dict with session list
|
|
329
394
|
"""
|
|
330
|
-
|
|
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 = """
|
|
331
487
|
SELECT
|
|
332
488
|
session_id,
|
|
333
489
|
COUNT(*) as memory_count,
|
|
334
490
|
MIN(created_at) as started_at,
|
|
335
491
|
MAX(created_at) as ended_at,
|
|
336
|
-
project_path,
|
|
337
|
-
project_name,
|
|
492
|
+
MAX(project_path) as project_path,
|
|
493
|
+
MAX(project_name) as project_name,
|
|
338
494
|
SUM(CASE WHEN outcome_status = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
339
495
|
SUM(CASE WHEN outcome_status = 'failed' THEN 1 ELSE 0 END) as failed_count,
|
|
340
|
-
SUM(CASE WHEN outcome_status = 'pending' THEN 1 ELSE 0 END) as pending_count,
|
|
496
|
+
SUM(CASE WHEN outcome_status = 'pending' OR outcome_status IS NULL THEN 1 ELSE 0 END) as pending_count,
|
|
341
497
|
AVG(confidence) as avg_confidence
|
|
342
498
|
FROM memories
|
|
343
499
|
WHERE session_id IS NOT NULL
|
|
500
|
+
AND session_id != ''
|
|
344
501
|
"""
|
|
345
|
-
|
|
502
|
+
mem_params = []
|
|
346
503
|
|
|
347
504
|
if project_path:
|
|
348
|
-
|
|
349
|
-
|
|
505
|
+
mem_query += " AND project_path = ?"
|
|
506
|
+
mem_params.append(project_path)
|
|
350
507
|
|
|
351
|
-
|
|
508
|
+
mem_query += """
|
|
352
509
|
GROUP BY session_id
|
|
353
510
|
ORDER BY MAX(created_at) DESC
|
|
354
511
|
LIMIT ?
|
|
355
512
|
"""
|
|
356
|
-
|
|
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)
|
|
357
539
|
|
|
358
|
-
|
|
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]
|
|
359
546
|
|
|
360
547
|
return {
|
|
361
548
|
"success": True,
|
|
362
|
-
"sessions":
|
|
363
|
-
"count": len(
|
|
549
|
+
"sessions": all_sessions,
|
|
550
|
+
"count": len(all_sessions)
|
|
364
551
|
}
|
|
365
552
|
|
|
366
553
|
|