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,199 @@
|
|
|
1
|
+
"""Rich Terminal UI for Claude Memory Agent.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Colorful startup splash screen with system stats
|
|
5
|
+
- Rich logging handler with colored levels
|
|
6
|
+
- Helper panels for status display
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
from rich.columns import Columns
|
|
17
|
+
from rich.logging import RichHandler
|
|
18
|
+
from rich.theme import Theme
|
|
19
|
+
from rich.align import Align
|
|
20
|
+
|
|
21
|
+
# Custom theme for the memory agent
|
|
22
|
+
MEMORY_THEME = Theme({
|
|
23
|
+
"info": "cyan",
|
|
24
|
+
"warning": "yellow",
|
|
25
|
+
"error": "bold red",
|
|
26
|
+
"success": "bold green",
|
|
27
|
+
"memory.hot": "bold red",
|
|
28
|
+
"memory.warm": "bold yellow",
|
|
29
|
+
"memory.cold": "bold blue",
|
|
30
|
+
"header": "bold magenta",
|
|
31
|
+
"muted": "dim white",
|
|
32
|
+
"accent": "bold cyan",
|
|
33
|
+
"value": "bold white",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
console = Console(theme=MEMORY_THEME)
|
|
37
|
+
|
|
38
|
+
# ASCII art logo
|
|
39
|
+
LOGO_MINI = r"""
|
|
40
|
+
___ _ _ __ __
|
|
41
|
+
/ __| |__ _ _ _ __| |___| \/ |___ _ __ ___ _ _ _ _
|
|
42
|
+
| (__| / _` | || / _` / -_) |\/| / -_) ' \/ _ \ '_| || |
|
|
43
|
+
\___|_\__,_|\_,_\__,_\___|_| |_\___|_|_|_\___/_| \_, |
|
|
44
|
+
|__/ """
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_splash(
|
|
48
|
+
version: str = "2.4.0",
|
|
49
|
+
port: int = 8102,
|
|
50
|
+
auth_enabled: bool = False,
|
|
51
|
+
auth_keys: int = 0,
|
|
52
|
+
queue_depth: int = 0,
|
|
53
|
+
curator_interval: int = 24,
|
|
54
|
+
embedding_cache_size: int = 500,
|
|
55
|
+
precompute_interval: int = 60,
|
|
56
|
+
consolidation_threshold: float = 0.85,
|
|
57
|
+
consolidation_interval: int = 12,
|
|
58
|
+
db_stats: Optional[Dict[str, Any]] = None,
|
|
59
|
+
):
|
|
60
|
+
"""Print the startup splash screen with system info.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
version: Server version
|
|
64
|
+
port: Server port
|
|
65
|
+
auth_enabled: Whether auth is enabled
|
|
66
|
+
auth_keys: Number of active auth keys
|
|
67
|
+
queue_depth: Retry queue depth
|
|
68
|
+
curator_interval: Curator check interval in hours
|
|
69
|
+
embedding_cache_size: LRU cache size
|
|
70
|
+
precompute_interval: Precompute loop interval in seconds
|
|
71
|
+
consolidation_threshold: Similarity threshold for consolidation
|
|
72
|
+
consolidation_interval: Consolidation loop interval in hours
|
|
73
|
+
db_stats: Optional database statistics dict
|
|
74
|
+
"""
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
# Logo panel
|
|
78
|
+
logo_text = Text(LOGO_MINI, style="bold cyan")
|
|
79
|
+
console.print(Panel(
|
|
80
|
+
Align.center(logo_text),
|
|
81
|
+
border_style="cyan",
|
|
82
|
+
padding=(0, 2),
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
# Version bar
|
|
86
|
+
version_text = Text()
|
|
87
|
+
version_text.append(" v", style="muted")
|
|
88
|
+
version_text.append(version, style="bold green")
|
|
89
|
+
version_text.append(" (CLaRa) ", style="muted")
|
|
90
|
+
version_text.append("|", style="muted")
|
|
91
|
+
version_text.append(f" Port {port} ", style="accent")
|
|
92
|
+
version_text.append("|", style="muted")
|
|
93
|
+
version_text.append(f" {datetime.now().strftime('%Y-%m-%d %H:%M')} ", style="muted")
|
|
94
|
+
console.print(Align.center(version_text))
|
|
95
|
+
console.print()
|
|
96
|
+
|
|
97
|
+
# Status panels in columns
|
|
98
|
+
panels = []
|
|
99
|
+
|
|
100
|
+
# Server panel
|
|
101
|
+
server_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
102
|
+
server_table.add_column("key", style="muted", width=18)
|
|
103
|
+
server_table.add_column("value", style="value")
|
|
104
|
+
|
|
105
|
+
auth_status = "[green]ENABLED[/green]" if auth_enabled else "[yellow]DISABLED[/yellow]"
|
|
106
|
+
server_table.add_row("Authentication", auth_status)
|
|
107
|
+
if auth_enabled:
|
|
108
|
+
server_table.add_row("Active Keys", str(auth_keys))
|
|
109
|
+
server_table.add_row("Retry Queue", f"{queue_depth} pending")
|
|
110
|
+
server_table.add_row("Curator", f"every {curator_interval}h")
|
|
111
|
+
|
|
112
|
+
panels.append(Panel(server_table, title="[bold]Server[/bold]", border_style="green", width=36))
|
|
113
|
+
|
|
114
|
+
# CLaRa Features panel
|
|
115
|
+
clara_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
116
|
+
clara_table.add_column("key", style="muted", width=18)
|
|
117
|
+
clara_table.add_column("value", style="value")
|
|
118
|
+
|
|
119
|
+
clara_table.add_row("Embedding Cache", f"{embedding_cache_size} slots")
|
|
120
|
+
clara_table.add_row("Precompute", f"every {precompute_interval}s")
|
|
121
|
+
clara_table.add_row("Consolidation", f"every {consolidation_interval}h")
|
|
122
|
+
clara_table.add_row("Similarity", f">= {consolidation_threshold}")
|
|
123
|
+
|
|
124
|
+
panels.append(Panel(clara_table, title="[bold]CLaRa Engine[/bold]", border_style="cyan", width=36))
|
|
125
|
+
|
|
126
|
+
# Database panel (if stats available)
|
|
127
|
+
if db_stats:
|
|
128
|
+
db_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
129
|
+
db_table.add_column("key", style="muted", width=18)
|
|
130
|
+
db_table.add_column("value", style="value")
|
|
131
|
+
|
|
132
|
+
total = db_stats.get('total_memories', '?')
|
|
133
|
+
db_table.add_row("Memories", f"[bold]{total}[/bold]")
|
|
134
|
+
|
|
135
|
+
# Tier breakdown if available
|
|
136
|
+
tier_counts = db_stats.get('tier_counts', {})
|
|
137
|
+
if tier_counts:
|
|
138
|
+
hot = tier_counts.get('hot', 0)
|
|
139
|
+
warm = tier_counts.get('warm', 0)
|
|
140
|
+
cold = tier_counts.get('cold', 0)
|
|
141
|
+
db_table.add_row("Tiers", f"[memory.hot]{hot}[/memory.hot] / [memory.warm]{warm}[/memory.warm] / [memory.cold]{cold}[/memory.cold]")
|
|
142
|
+
else:
|
|
143
|
+
db_table.add_row("Tiers", "[muted]not evaluated[/muted]")
|
|
144
|
+
|
|
145
|
+
patterns = db_stats.get('total_patterns', '?')
|
|
146
|
+
db_table.add_row("Patterns", str(patterns))
|
|
147
|
+
|
|
148
|
+
panels.append(Panel(db_table, title="[bold]Database[/bold]", border_style="yellow", width=36))
|
|
149
|
+
|
|
150
|
+
console.print(Columns(panels, align="center", padding=(0, 1)))
|
|
151
|
+
console.print()
|
|
152
|
+
|
|
153
|
+
# Ready message
|
|
154
|
+
console.print(
|
|
155
|
+
Align.center(
|
|
156
|
+
Text.assemble(
|
|
157
|
+
(" Ready ", "bold white on green"),
|
|
158
|
+
(" ", ""),
|
|
159
|
+
(f"http://localhost:{port}", "bold underline cyan"),
|
|
160
|
+
(" | ", "muted"),
|
|
161
|
+
(f"Dashboard: http://localhost:{port}/dashboard", "cyan"),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
console.print()
|
|
166
|
+
_print_separator()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _print_separator():
|
|
170
|
+
"""Print a subtle separator line."""
|
|
171
|
+
console.print("[muted]" + "-" * 70 + "[/muted]", justify="center")
|
|
172
|
+
console.print()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def setup_rich_logging(level: str = "INFO") -> logging.Handler:
|
|
176
|
+
"""Set up rich logging handler for the application.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
RichHandler instance configured for the memory agent
|
|
180
|
+
"""
|
|
181
|
+
handler = RichHandler(
|
|
182
|
+
console=console,
|
|
183
|
+
show_time=True,
|
|
184
|
+
show_level=True,
|
|
185
|
+
show_path=False,
|
|
186
|
+
markup=True,
|
|
187
|
+
rich_tracebacks=True,
|
|
188
|
+
tracebacks_show_locals=False,
|
|
189
|
+
log_time_format="[%H:%M:%S]",
|
|
190
|
+
)
|
|
191
|
+
handler.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
192
|
+
|
|
193
|
+
# Format
|
|
194
|
+
formatter = logging.Formatter("%(message)s")
|
|
195
|
+
handler.setFormatter(formatter)
|
|
196
|
+
|
|
197
|
+
return handler
|
|
198
|
+
|
|
199
|
+
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Hierarchical Memory Tier Manager - CLaRa-inspired multi-stage processing.
|
|
2
|
+
|
|
3
|
+
Manages three tiers of memories with different search strategies:
|
|
4
|
+
- Hot: Recent, high-importance, frequently accessed. Full semantic + keyword search, flat FAISS.
|
|
5
|
+
- Warm: Older but relevant. Semantic search on compressed content, IVF FAISS.
|
|
6
|
+
- Cold: Archived, low importance. Keyword-only search, no FAISS index.
|
|
7
|
+
|
|
8
|
+
Permanent types (decision, preference, code) with importance >= 5 stay hot.
|
|
9
|
+
"""
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Dict, Any, List, Optional
|
|
13
|
+
|
|
14
|
+
from config import config
|
|
15
|
+
from services.memory_decay import DECAY_LIFESPANS
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Tier constants
|
|
20
|
+
TIER_HOT = 'hot'
|
|
21
|
+
TIER_WARM = 'warm'
|
|
22
|
+
TIER_COLD = 'cold'
|
|
23
|
+
|
|
24
|
+
# Permanent types that resist demotion
|
|
25
|
+
PERMANENT_TYPES = {'decision', 'preference', 'code'}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TierManager:
|
|
29
|
+
"""Manages memory tier promotion and demotion.
|
|
30
|
+
|
|
31
|
+
Scoring algorithm considers:
|
|
32
|
+
- Age (days since creation)
|
|
33
|
+
- Importance (1-10)
|
|
34
|
+
- Access frequency (access_count)
|
|
35
|
+
- Memory type (permanent vs ephemeral)
|
|
36
|
+
- Decay factor
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, db):
|
|
40
|
+
self.db = db
|
|
41
|
+
self.hot_max_age = config.TIER_HOT_MAX_AGE_DAYS
|
|
42
|
+
self.hot_min_importance = config.TIER_HOT_MIN_IMPORTANCE
|
|
43
|
+
self.warm_max_age = config.TIER_WARM_MAX_AGE_DAYS
|
|
44
|
+
|
|
45
|
+
def evaluate_tier(self, memory: dict) -> str:
|
|
46
|
+
"""Evaluate which tier a memory belongs in.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
memory: Dict with type, importance, created_at, access_count, decay_factor
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tier string: 'hot', 'warm', or 'cold'
|
|
53
|
+
"""
|
|
54
|
+
memory_type = memory.get('type', 'chunk')
|
|
55
|
+
importance = memory.get('importance', 5)
|
|
56
|
+
access_count = memory.get('access_count', 0) or 0
|
|
57
|
+
decay_factor = memory.get('decay_factor', 1.0) or 1.0
|
|
58
|
+
|
|
59
|
+
# Permanent types with importance >= 5 always stay hot
|
|
60
|
+
if memory_type in PERMANENT_TYPES and importance >= 5:
|
|
61
|
+
return TIER_HOT
|
|
62
|
+
|
|
63
|
+
# Calculate age
|
|
64
|
+
created_at = memory.get('created_at')
|
|
65
|
+
age_days = self._calculate_age_days(created_at)
|
|
66
|
+
|
|
67
|
+
# Hot tier criteria:
|
|
68
|
+
# - Recent (< hot_max_age days) OR
|
|
69
|
+
# - High importance (>= hot_min_importance) OR
|
|
70
|
+
# - Frequently accessed (access_count >= 5 in last 14 days)
|
|
71
|
+
if age_days < self.hot_max_age:
|
|
72
|
+
return TIER_HOT
|
|
73
|
+
if importance >= self.hot_min_importance:
|
|
74
|
+
return TIER_HOT
|
|
75
|
+
if access_count >= 5 and age_days < self.hot_max_age * 2:
|
|
76
|
+
return TIER_HOT
|
|
77
|
+
|
|
78
|
+
# Warm tier criteria:
|
|
79
|
+
# - Age < warm_max_age AND (importance >= 3 OR access_count >= 2)
|
|
80
|
+
if age_days < self.warm_max_age:
|
|
81
|
+
if importance >= 3 or access_count >= 2:
|
|
82
|
+
return TIER_WARM
|
|
83
|
+
# Low importance, low access but still within warm window
|
|
84
|
+
if decay_factor > 0.3:
|
|
85
|
+
return TIER_WARM
|
|
86
|
+
|
|
87
|
+
# Cold: everything else
|
|
88
|
+
return TIER_COLD
|
|
89
|
+
|
|
90
|
+
def _calculate_age_days(self, created_at: Optional[str]) -> float:
|
|
91
|
+
"""Calculate age in days from a timestamp string."""
|
|
92
|
+
if not created_at:
|
|
93
|
+
return 0.0
|
|
94
|
+
try:
|
|
95
|
+
created_dt = datetime.fromisoformat(
|
|
96
|
+
created_at.replace('Z', '+00:00')
|
|
97
|
+
).replace(tzinfo=None)
|
|
98
|
+
return (datetime.now() - created_dt).total_seconds() / 86400.0
|
|
99
|
+
except (ValueError, TypeError, AttributeError):
|
|
100
|
+
return 0.0
|
|
101
|
+
|
|
102
|
+
async def run_tier_maintenance(self, skip_recent_hours: int = 24) -> Dict[str, Any]:
|
|
103
|
+
"""Evaluate and update tiers for all memories.
|
|
104
|
+
|
|
105
|
+
Combines with decay evaluation for efficiency (single pass over all memories).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
skip_recent_hours: Skip memories whose tier was evaluated within this window
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict with maintenance statistics
|
|
112
|
+
"""
|
|
113
|
+
cursor = self.db.conn.cursor()
|
|
114
|
+
|
|
115
|
+
# Fetch all memories that need tier evaluation
|
|
116
|
+
# Skip those evaluated recently (tier_changed_at within skip window)
|
|
117
|
+
cutoff = (datetime.now() - timedelta(hours=skip_recent_hours)).isoformat()
|
|
118
|
+
|
|
119
|
+
cursor.execute("""
|
|
120
|
+
SELECT id, type, importance, access_count, decay_factor, created_at,
|
|
121
|
+
last_accessed, tier, tier_changed_at
|
|
122
|
+
FROM memories
|
|
123
|
+
WHERE tier_changed_at IS NULL OR tier_changed_at < ?
|
|
124
|
+
""", (cutoff,))
|
|
125
|
+
|
|
126
|
+
rows = cursor.fetchall()
|
|
127
|
+
|
|
128
|
+
stats = {
|
|
129
|
+
'evaluated': 0,
|
|
130
|
+
'promoted': 0,
|
|
131
|
+
'demoted': 0,
|
|
132
|
+
'unchanged': 0,
|
|
133
|
+
'tier_counts': {TIER_HOT: 0, TIER_WARM: 0, TIER_COLD: 0}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
updates = []
|
|
137
|
+
|
|
138
|
+
for row in rows:
|
|
139
|
+
stats['evaluated'] += 1
|
|
140
|
+
memory_dict = dict(row)
|
|
141
|
+
new_tier = self.evaluate_tier(memory_dict)
|
|
142
|
+
old_tier = row['tier'] or TIER_HOT # Default to hot for unmigrated
|
|
143
|
+
|
|
144
|
+
stats['tier_counts'][new_tier] = stats['tier_counts'].get(new_tier, 0) + 1
|
|
145
|
+
|
|
146
|
+
if new_tier != old_tier:
|
|
147
|
+
updates.append((new_tier, datetime.now().isoformat(), row['id']))
|
|
148
|
+
if self._tier_rank(new_tier) < self._tier_rank(old_tier):
|
|
149
|
+
stats['promoted'] += 1
|
|
150
|
+
else:
|
|
151
|
+
stats['demoted'] += 1
|
|
152
|
+
else:
|
|
153
|
+
stats['unchanged'] += 1
|
|
154
|
+
# Still update tier_changed_at to avoid re-evaluation
|
|
155
|
+
updates.append((new_tier, datetime.now().isoformat(), row['id']))
|
|
156
|
+
|
|
157
|
+
# Batch update
|
|
158
|
+
if updates:
|
|
159
|
+
cursor.executemany(
|
|
160
|
+
"UPDATE memories SET tier = ?, tier_changed_at = ? WHERE id = ?",
|
|
161
|
+
updates
|
|
162
|
+
)
|
|
163
|
+
self.db.conn.commit()
|
|
164
|
+
|
|
165
|
+
stats['timestamp'] = datetime.now().isoformat()
|
|
166
|
+
return stats
|
|
167
|
+
|
|
168
|
+
def _tier_rank(self, tier: str) -> int:
|
|
169
|
+
"""Numeric rank for tier comparison (lower = hotter)."""
|
|
170
|
+
return {TIER_HOT: 0, TIER_WARM: 1, TIER_COLD: 2}.get(tier, 1)
|
|
171
|
+
|
|
172
|
+
async def promote_on_access(self, memory_id: int) -> Optional[str]:
|
|
173
|
+
"""Re-evaluate and promote a memory's tier when it is accessed.
|
|
174
|
+
|
|
175
|
+
Called after search results are returned to ensure frequently-accessed
|
|
176
|
+
memories don't stay cold/warm until the next batch maintenance run.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
memory_id: ID of the accessed memory
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
New tier if promoted, None if unchanged
|
|
183
|
+
"""
|
|
184
|
+
cursor = self.db.conn.cursor()
|
|
185
|
+
cursor.execute(
|
|
186
|
+
"SELECT id, type, importance, access_count, decay_factor, created_at, tier FROM memories WHERE id = ?",
|
|
187
|
+
(memory_id,)
|
|
188
|
+
)
|
|
189
|
+
row = cursor.fetchone()
|
|
190
|
+
if not row:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
current_tier = row['tier'] or TIER_HOT
|
|
194
|
+
new_tier = self.evaluate_tier(dict(row))
|
|
195
|
+
|
|
196
|
+
if self._tier_rank(new_tier) < self._tier_rank(current_tier):
|
|
197
|
+
cursor.execute(
|
|
198
|
+
"UPDATE memories SET tier = ?, tier_changed_at = ? WHERE id = ?",
|
|
199
|
+
(new_tier, datetime.now().isoformat(), memory_id)
|
|
200
|
+
)
|
|
201
|
+
self.db.conn.commit()
|
|
202
|
+
logger.info(f"Memory {memory_id} promoted from {current_tier} to {new_tier}")
|
|
203
|
+
return new_tier
|
|
204
|
+
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
async def get_tier_stats(self) -> Dict[str, Any]:
|
|
208
|
+
"""Get distribution of memories across tiers."""
|
|
209
|
+
cursor = self.db.conn.cursor()
|
|
210
|
+
|
|
211
|
+
cursor.execute("""
|
|
212
|
+
SELECT COALESCE(tier, 'hot') as tier, COUNT(*) as count,
|
|
213
|
+
AVG(importance) as avg_importance,
|
|
214
|
+
AVG(access_count) as avg_access_count
|
|
215
|
+
FROM memories
|
|
216
|
+
GROUP BY COALESCE(tier, 'hot')
|
|
217
|
+
""")
|
|
218
|
+
|
|
219
|
+
tiers = {}
|
|
220
|
+
total = 0
|
|
221
|
+
for row in cursor.fetchall():
|
|
222
|
+
tier = row['tier']
|
|
223
|
+
count = row['count']
|
|
224
|
+
total += count
|
|
225
|
+
tiers[tier] = {
|
|
226
|
+
'count': count,
|
|
227
|
+
'avg_importance': round(row['avg_importance'] or 0, 2),
|
|
228
|
+
'avg_access_count': round(row['avg_access_count'] or 0, 2)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
'total_memories': total,
|
|
233
|
+
'tiers': tiers,
|
|
234
|
+
'timestamp': datetime.now().isoformat()
|
|
235
|
+
}
|
package/services/websocket.py
CHANGED
|
@@ -14,6 +14,17 @@ from dataclasses import dataclass, field
|
|
|
14
14
|
from fastapi import WebSocket, WebSocketDisconnect
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _normalize_ws_path(path: Optional[str]) -> Optional[str]:
|
|
18
|
+
"""Normalize path for WebSocket comparison.
|
|
19
|
+
|
|
20
|
+
Converts backslashes to forward slashes and removes trailing slashes.
|
|
21
|
+
This ensures consistent path comparison across platforms.
|
|
22
|
+
"""
|
|
23
|
+
if not path:
|
|
24
|
+
return None
|
|
25
|
+
return path.replace("\\", "/").rstrip("/")
|
|
26
|
+
|
|
27
|
+
|
|
17
28
|
@dataclass
|
|
18
29
|
class WebSocketClient:
|
|
19
30
|
"""Represents a connected WebSocket client."""
|
|
@@ -84,7 +95,8 @@ class WebSocketManager:
|
|
|
84
95
|
|
|
85
96
|
client = self.clients[client_id]
|
|
86
97
|
client.subscriptions = set(event_types)
|
|
87
|
-
|
|
98
|
+
# Normalize path for consistent comparison
|
|
99
|
+
client.project_filter = _normalize_ws_path(project_path)
|
|
88
100
|
|
|
89
101
|
await self._send_to_client(client_id, {
|
|
90
102
|
"type": "subscribed",
|
|
@@ -101,15 +113,18 @@ class WebSocketManager:
|
|
|
101
113
|
data: Event data payload
|
|
102
114
|
project_path: Project this event relates to (for filtering)
|
|
103
115
|
"""
|
|
116
|
+
# Normalize the project path for consistent comparison
|
|
117
|
+
normalized_project = _normalize_ws_path(project_path)
|
|
118
|
+
|
|
104
119
|
message = {
|
|
105
120
|
"type": event_type,
|
|
106
121
|
"data": data,
|
|
107
|
-
"project_path":
|
|
122
|
+
"project_path": normalized_project,
|
|
108
123
|
"timestamp": time.time()
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
# Debug logging
|
|
112
|
-
print(f"[WS] Broadcasting {event_type} to {len(self.clients)} clients, project={
|
|
127
|
+
print(f"[WS] Broadcasting {event_type} to {len(self.clients)} clients, project={normalized_project}")
|
|
113
128
|
|
|
114
129
|
# Send to all matching clients
|
|
115
130
|
disconnected = []
|
|
@@ -120,11 +135,11 @@ class WebSocketManager:
|
|
|
120
135
|
print(f"[WS] Skipping {client_id}: not subscribed to {event_type}")
|
|
121
136
|
continue
|
|
122
137
|
|
|
123
|
-
# Check project filter
|
|
138
|
+
# Check project filter (both are already normalized)
|
|
124
139
|
# If project_path is None, send to all clients (global event)
|
|
125
140
|
# If project_path is set, only send to matching clients
|
|
126
|
-
if
|
|
127
|
-
print(f"[WS] Skipping {client_id}: project mismatch ({client.project_filter} != {
|
|
141
|
+
if normalized_project and client.project_filter and client.project_filter != normalized_project:
|
|
142
|
+
print(f"[WS] Skipping {client_id}: project mismatch ({client.project_filter} != {normalized_project})")
|
|
128
143
|
continue
|
|
129
144
|
|
|
130
145
|
try:
|
|
@@ -217,11 +232,16 @@ class EventTypes:
|
|
|
217
232
|
"""Standard event types for broadcasting."""
|
|
218
233
|
# Memory events
|
|
219
234
|
MEMORY_STORED = "memory_stored"
|
|
235
|
+
MEMORY_UPDATED = "memory_updated"
|
|
220
236
|
MEMORY_SEARCHED = "memory_searched"
|
|
221
237
|
MEMORY_DELETED = "memory_deleted"
|
|
222
238
|
MEMORY_ARCHIVED = "memory_archived"
|
|
223
239
|
MEMORY_RESTORED = "memory_restored"
|
|
224
240
|
|
|
241
|
+
# Relationship/Link events
|
|
242
|
+
LINK_CREATED = "link_created"
|
|
243
|
+
LINK_DELETED = "link_deleted"
|
|
244
|
+
|
|
225
245
|
# Timeline events
|
|
226
246
|
TIMELINE_LOGGED = "timeline_logged"
|
|
227
247
|
CHECKPOINT_CREATED = "checkpoint_created"
|
package/skills/__init__.py
CHANGED
|
@@ -2,5 +2,25 @@ from .store import store_memory
|
|
|
2
2
|
from .retrieve import retrieve_memory
|
|
3
3
|
from .search import semantic_search
|
|
4
4
|
from .summarize import summarize_session
|
|
5
|
+
from .confidence_tracker import (
|
|
6
|
+
report_solution_outcome,
|
|
7
|
+
get_reliability_stats,
|
|
8
|
+
get_unreliable_memories,
|
|
9
|
+
reset_memory_reliability,
|
|
10
|
+
memory_worked,
|
|
11
|
+
memory_failed
|
|
12
|
+
)
|
|
5
13
|
|
|
6
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"store_memory",
|
|
16
|
+
"retrieve_memory",
|
|
17
|
+
"semantic_search",
|
|
18
|
+
"summarize_session",
|
|
19
|
+
# Self-correcting confidence
|
|
20
|
+
"report_solution_outcome",
|
|
21
|
+
"get_reliability_stats",
|
|
22
|
+
"get_unreliable_memories",
|
|
23
|
+
"reset_memory_reliability",
|
|
24
|
+
"memory_worked",
|
|
25
|
+
"memory_failed"
|
|
26
|
+
]
|