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,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
+ }
@@ -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
- client.project_filter = project_path
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": 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={project_path}")
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 project_path and client.project_filter and client.project_filter != project_path:
127
- print(f"[WS] Skipping {client_id}: project mismatch ({client.project_filter} != {project_path})")
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"
@@ -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__ = ["store_memory", "retrieve_memory", "semantic_search", "summarize_session"]
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
+ ]