superlocalmemory 2.3.7 → 2.4.1
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/CHANGELOG.md +66 -0
- package/README.md +53 -6
- package/hooks/memory-profile-skill.js +7 -18
- package/mcp_server.py +74 -12
- package/package.json +2 -1
- package/src/auto_backup.py +424 -0
- package/src/graph_engine.py +459 -44
- package/src/memory-profiles.py +321 -243
- package/src/memory_store_v2.py +82 -31
- package/src/pattern_learner.py +126 -44
- package/src/setup_validator.py +8 -1
- package/ui/app.js +526 -17
- package/ui/index.html +182 -1
- package/ui_server.py +356 -55
- package/src/__pycache__/cache_manager.cpython-312.pyc +0 -0
- package/src/__pycache__/embedding_engine.cpython-312.pyc +0 -0
- package/src/__pycache__/graph_engine.cpython-312.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-312.pyc +0 -0
- package/src/__pycache__/hybrid_search.cpython-312.pyc +0 -0
- package/src/__pycache__/memory-profiles.cpython-312.pyc +0 -0
- package/src/__pycache__/memory-reset.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_compression.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_store_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/migrate_v1_to_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/pattern_learner.cpython-312.pyc +0 -0
- package/src/__pycache__/query_optimizer.cpython-312.pyc +0 -0
- package/src/__pycache__/search_engine_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/setup_validator.cpython-312.pyc +0 -0
- package/src/__pycache__/tree_manager.cpython-312.pyc +0 -0
package/src/memory_store_v2.py
CHANGED
|
@@ -53,21 +53,41 @@ class MemoryStoreV2:
|
|
|
53
53
|
- Backward compatible with V1 API
|
|
54
54
|
"""
|
|
55
55
|
|
|
56
|
-
def __init__(self, db_path: Optional[Path] = None):
|
|
56
|
+
def __init__(self, db_path: Optional[Path] = None, profile: Optional[str] = None):
|
|
57
57
|
"""
|
|
58
58
|
Initialize MemoryStore V2.
|
|
59
59
|
|
|
60
60
|
Args:
|
|
61
61
|
db_path: Optional custom database path (defaults to ~/.claude-memory/memory.db)
|
|
62
|
+
profile: Optional profile override. If None, reads from profiles.json config.
|
|
62
63
|
"""
|
|
63
64
|
self.db_path = db_path or DB_PATH
|
|
64
65
|
self.vectors_path = VECTORS_PATH
|
|
66
|
+
self._profile_override = profile
|
|
65
67
|
self._init_db()
|
|
66
68
|
self.vectorizer = None
|
|
67
69
|
self.vectors = None
|
|
68
70
|
self.memory_ids = []
|
|
69
71
|
self._load_vectors()
|
|
70
72
|
|
|
73
|
+
def _get_active_profile(self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Get the currently active profile name.
|
|
76
|
+
Reads from profiles.json config file. Falls back to 'default'.
|
|
77
|
+
"""
|
|
78
|
+
if self._profile_override:
|
|
79
|
+
return self._profile_override
|
|
80
|
+
|
|
81
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
82
|
+
if config_file.exists():
|
|
83
|
+
try:
|
|
84
|
+
with open(config_file, 'r') as f:
|
|
85
|
+
config = json.load(f)
|
|
86
|
+
return config.get('active_profile', 'default')
|
|
87
|
+
except (json.JSONDecodeError, IOError):
|
|
88
|
+
pass
|
|
89
|
+
return 'default'
|
|
90
|
+
|
|
71
91
|
def _init_db(self):
|
|
72
92
|
"""Initialize SQLite database with V2 schema extensions."""
|
|
73
93
|
conn = sqlite3.connect(self.db_path)
|
|
@@ -129,7 +149,8 @@ class MemoryStoreV2:
|
|
|
129
149
|
'last_accessed': 'TIMESTAMP',
|
|
130
150
|
'access_count': 'INTEGER DEFAULT 0',
|
|
131
151
|
'content_hash': 'TEXT',
|
|
132
|
-
'cluster_id': 'INTEGER'
|
|
152
|
+
'cluster_id': 'INTEGER',
|
|
153
|
+
'profile': 'TEXT DEFAULT "default"'
|
|
133
154
|
}
|
|
134
155
|
|
|
135
156
|
for col_name, col_type in v2_columns.items():
|
|
@@ -190,7 +211,8 @@ class MemoryStoreV2:
|
|
|
190
211
|
('idx_tree_path', 'tree_path'),
|
|
191
212
|
('idx_cluster', 'cluster_id'),
|
|
192
213
|
('idx_last_accessed', 'last_accessed'),
|
|
193
|
-
('idx_parent_id', 'parent_id')
|
|
214
|
+
('idx_parent_id', 'parent_id'),
|
|
215
|
+
('idx_profile', 'profile')
|
|
194
216
|
]
|
|
195
217
|
|
|
196
218
|
for idx_name, col_name in v2_indexes:
|
|
@@ -303,6 +325,7 @@ class MemoryStoreV2:
|
|
|
303
325
|
importance = max(1, min(10, importance)) # Clamp to valid range
|
|
304
326
|
|
|
305
327
|
content_hash = self._content_hash(content)
|
|
328
|
+
active_profile = self._get_active_profile()
|
|
306
329
|
|
|
307
330
|
conn = sqlite3.connect(self.db_path)
|
|
308
331
|
cursor = conn.cursor()
|
|
@@ -316,9 +339,9 @@ class MemoryStoreV2:
|
|
|
316
339
|
content, summary, project_path, project_name, tags, category,
|
|
317
340
|
parent_id, tree_path, depth,
|
|
318
341
|
memory_type, importance, content_hash,
|
|
319
|
-
last_accessed, access_count
|
|
342
|
+
last_accessed, access_count, profile
|
|
320
343
|
)
|
|
321
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
322
345
|
''', (
|
|
323
346
|
content,
|
|
324
347
|
summary,
|
|
@@ -333,7 +356,8 @@ class MemoryStoreV2:
|
|
|
333
356
|
importance,
|
|
334
357
|
content_hash,
|
|
335
358
|
datetime.now().isoformat(),
|
|
336
|
-
0
|
|
359
|
+
0,
|
|
360
|
+
active_profile
|
|
337
361
|
))
|
|
338
362
|
memory_id = cursor.lastrowid
|
|
339
363
|
|
|
@@ -350,6 +374,14 @@ class MemoryStoreV2:
|
|
|
350
374
|
# Rebuild vectors after adding
|
|
351
375
|
self._rebuild_vectors()
|
|
352
376
|
|
|
377
|
+
# Auto-backup check (non-blocking)
|
|
378
|
+
try:
|
|
379
|
+
from auto_backup import AutoBackup
|
|
380
|
+
backup = AutoBackup()
|
|
381
|
+
backup.check_and_backup()
|
|
382
|
+
except Exception:
|
|
383
|
+
pass # Backup failure must never break memory operations
|
|
384
|
+
|
|
353
385
|
return memory_id
|
|
354
386
|
|
|
355
387
|
except sqlite3.IntegrityError:
|
|
@@ -410,6 +442,7 @@ class MemoryStoreV2:
|
|
|
410
442
|
List of memory dictionaries with scores
|
|
411
443
|
"""
|
|
412
444
|
results = []
|
|
445
|
+
active_profile = self._get_active_profile()
|
|
413
446
|
|
|
414
447
|
# Method 1: TF-IDF semantic search
|
|
415
448
|
if SKLEARN_AVAILABLE and self.vectorizer is not None and self.vectors is not None:
|
|
@@ -432,8 +465,8 @@ class MemoryStoreV2:
|
|
|
432
465
|
category, parent_id, tree_path, depth,
|
|
433
466
|
memory_type, importance, created_at, cluster_id,
|
|
434
467
|
last_accessed, access_count
|
|
435
|
-
FROM memories WHERE id = ?
|
|
436
|
-
''', (memory_id,))
|
|
468
|
+
FROM memories WHERE id = ? AND profile = ?
|
|
469
|
+
''', (memory_id, active_profile))
|
|
437
470
|
row = cursor.fetchone()
|
|
438
471
|
|
|
439
472
|
if row and self._apply_filters(row, project_path, memory_type,
|
|
@@ -460,10 +493,10 @@ class MemoryStoreV2:
|
|
|
460
493
|
m.last_accessed, m.access_count
|
|
461
494
|
FROM memories m
|
|
462
495
|
JOIN memories_fts fts ON m.id = fts.rowid
|
|
463
|
-
WHERE memories_fts MATCH ?
|
|
496
|
+
WHERE memories_fts MATCH ? AND m.profile = ?
|
|
464
497
|
ORDER BY rank
|
|
465
498
|
LIMIT ?
|
|
466
|
-
''', (fts_query, limit))
|
|
499
|
+
''', (fts_query, active_profile, limit))
|
|
467
500
|
|
|
468
501
|
existing_ids = {r['id'] for r in results}
|
|
469
502
|
|
|
@@ -570,6 +603,7 @@ class MemoryStoreV2:
|
|
|
570
603
|
Returns:
|
|
571
604
|
List of memories with tree structure
|
|
572
605
|
"""
|
|
606
|
+
active_profile = self._get_active_profile()
|
|
573
607
|
conn = sqlite3.connect(self.db_path)
|
|
574
608
|
cursor = conn.cursor()
|
|
575
609
|
|
|
@@ -580,9 +614,9 @@ class MemoryStoreV2:
|
|
|
580
614
|
category, parent_id, tree_path, depth, memory_type, importance,
|
|
581
615
|
created_at, cluster_id, last_accessed, access_count
|
|
582
616
|
FROM memories
|
|
583
|
-
WHERE parent_id IS NULL AND depth <= ?
|
|
617
|
+
WHERE parent_id IS NULL AND depth <= ? AND profile = ?
|
|
584
618
|
ORDER BY tree_path
|
|
585
|
-
''', (max_depth,))
|
|
619
|
+
''', (max_depth, active_profile))
|
|
586
620
|
else:
|
|
587
621
|
# Get subtree under specific parent
|
|
588
622
|
cursor.execute('''
|
|
@@ -649,6 +683,7 @@ class MemoryStoreV2:
|
|
|
649
683
|
Returns:
|
|
650
684
|
List of memories in the cluster
|
|
651
685
|
"""
|
|
686
|
+
active_profile = self._get_active_profile()
|
|
652
687
|
conn = sqlite3.connect(self.db_path)
|
|
653
688
|
cursor = conn.cursor()
|
|
654
689
|
|
|
@@ -657,9 +692,9 @@ class MemoryStoreV2:
|
|
|
657
692
|
category, parent_id, tree_path, depth, memory_type, importance,
|
|
658
693
|
created_at, cluster_id, last_accessed, access_count
|
|
659
694
|
FROM memories
|
|
660
|
-
WHERE cluster_id = ?
|
|
695
|
+
WHERE cluster_id = ? AND profile = ?
|
|
661
696
|
ORDER BY importance DESC, created_at DESC
|
|
662
|
-
''', (cluster_id,))
|
|
697
|
+
''', (cluster_id, active_profile))
|
|
663
698
|
|
|
664
699
|
results = []
|
|
665
700
|
for row in cursor.fetchall():
|
|
@@ -675,10 +710,11 @@ class MemoryStoreV2:
|
|
|
675
710
|
self._rebuild_vectors()
|
|
676
711
|
|
|
677
712
|
def _rebuild_vectors(self):
|
|
678
|
-
"""Rebuild TF-IDF vectors from
|
|
713
|
+
"""Rebuild TF-IDF vectors from active profile memories (V1 compatible, backward compatible)."""
|
|
679
714
|
if not SKLEARN_AVAILABLE:
|
|
680
715
|
return
|
|
681
716
|
|
|
717
|
+
active_profile = self._get_active_profile()
|
|
682
718
|
conn = sqlite3.connect(self.db_path)
|
|
683
719
|
cursor = conn.cursor()
|
|
684
720
|
|
|
@@ -686,9 +722,13 @@ class MemoryStoreV2:
|
|
|
686
722
|
cursor.execute("PRAGMA table_info(memories)")
|
|
687
723
|
columns = {row[1] for row in cursor.fetchall()}
|
|
688
724
|
|
|
689
|
-
# Build SELECT query based on available columns
|
|
725
|
+
# Build SELECT query based on available columns, filtered by profile
|
|
726
|
+
has_profile = 'profile' in columns
|
|
690
727
|
if 'summary' in columns:
|
|
691
|
-
|
|
728
|
+
if has_profile:
|
|
729
|
+
cursor.execute('SELECT id, content, summary FROM memories WHERE profile = ?', (active_profile,))
|
|
730
|
+
else:
|
|
731
|
+
cursor.execute('SELECT id, content, summary FROM memories')
|
|
692
732
|
rows = cursor.fetchall()
|
|
693
733
|
texts = [f"{row[1]} {row[2] or ''}" for row in rows]
|
|
694
734
|
else:
|
|
@@ -720,7 +760,8 @@ class MemoryStoreV2:
|
|
|
720
760
|
json.dump(self.memory_ids, f)
|
|
721
761
|
|
|
722
762
|
def get_recent(self, limit: int = 10, project_path: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
723
|
-
"""Get most recent memories (V1 compatible)."""
|
|
763
|
+
"""Get most recent memories (V1 compatible, profile-aware)."""
|
|
764
|
+
active_profile = self._get_active_profile()
|
|
724
765
|
conn = sqlite3.connect(self.db_path)
|
|
725
766
|
cursor = conn.cursor()
|
|
726
767
|
|
|
@@ -730,19 +771,20 @@ class MemoryStoreV2:
|
|
|
730
771
|
category, parent_id, tree_path, depth, memory_type, importance,
|
|
731
772
|
created_at, cluster_id, last_accessed, access_count
|
|
732
773
|
FROM memories
|
|
733
|
-
WHERE project_path = ?
|
|
774
|
+
WHERE project_path = ? AND profile = ?
|
|
734
775
|
ORDER BY created_at DESC
|
|
735
776
|
LIMIT ?
|
|
736
|
-
''', (project_path, limit))
|
|
777
|
+
''', (project_path, active_profile, limit))
|
|
737
778
|
else:
|
|
738
779
|
cursor.execute('''
|
|
739
780
|
SELECT id, content, summary, project_path, project_name, tags,
|
|
740
781
|
category, parent_id, tree_path, depth, memory_type, importance,
|
|
741
782
|
created_at, cluster_id, last_accessed, access_count
|
|
742
783
|
FROM memories
|
|
784
|
+
WHERE profile = ?
|
|
743
785
|
ORDER BY created_at DESC
|
|
744
786
|
LIMIT ?
|
|
745
|
-
''', (limit
|
|
787
|
+
''', (active_profile, limit))
|
|
746
788
|
|
|
747
789
|
results = []
|
|
748
790
|
for row in cursor.fetchall():
|
|
@@ -789,7 +831,8 @@ class MemoryStoreV2:
|
|
|
789
831
|
return deleted
|
|
790
832
|
|
|
791
833
|
def list_all(self, limit: int = 50) -> List[Dict[str, Any]]:
|
|
792
|
-
"""List all memories with short previews (V1 compatible)."""
|
|
834
|
+
"""List all memories with short previews (V1 compatible, profile-aware)."""
|
|
835
|
+
active_profile = self._get_active_profile()
|
|
793
836
|
conn = sqlite3.connect(self.db_path)
|
|
794
837
|
cursor = conn.cursor()
|
|
795
838
|
|
|
@@ -798,9 +841,10 @@ class MemoryStoreV2:
|
|
|
798
841
|
category, parent_id, tree_path, depth, memory_type, importance,
|
|
799
842
|
created_at, cluster_id, last_accessed, access_count
|
|
800
843
|
FROM memories
|
|
844
|
+
WHERE profile = ?
|
|
801
845
|
ORDER BY created_at DESC
|
|
802
846
|
LIMIT ?
|
|
803
|
-
''', (limit
|
|
847
|
+
''', (active_profile, limit))
|
|
804
848
|
|
|
805
849
|
results = []
|
|
806
850
|
for row in cursor.fetchall():
|
|
@@ -817,35 +861,42 @@ class MemoryStoreV2:
|
|
|
817
861
|
return results
|
|
818
862
|
|
|
819
863
|
def get_stats(self) -> Dict[str, Any]:
|
|
820
|
-
"""Get memory store statistics (V1 compatible with V2 extensions)."""
|
|
864
|
+
"""Get memory store statistics (V1 compatible with V2 extensions, profile-aware)."""
|
|
865
|
+
active_profile = self._get_active_profile()
|
|
821
866
|
conn = sqlite3.connect(self.db_path)
|
|
822
867
|
cursor = conn.cursor()
|
|
823
868
|
|
|
824
|
-
cursor.execute('SELECT COUNT(*) FROM memories')
|
|
869
|
+
cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?', (active_profile,))
|
|
825
870
|
total_memories = cursor.fetchone()[0]
|
|
826
871
|
|
|
827
|
-
cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL')
|
|
872
|
+
cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL AND profile = ?', (active_profile,))
|
|
828
873
|
total_projects = cursor.fetchone()[0]
|
|
829
874
|
|
|
830
|
-
cursor.execute('SELECT memory_type, COUNT(*) FROM memories GROUP BY memory_type')
|
|
875
|
+
cursor.execute('SELECT memory_type, COUNT(*) FROM memories WHERE profile = ? GROUP BY memory_type', (active_profile,))
|
|
831
876
|
by_type = dict(cursor.fetchall())
|
|
832
877
|
|
|
833
|
-
cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL GROUP BY category')
|
|
878
|
+
cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL AND profile = ? GROUP BY category', (active_profile,))
|
|
834
879
|
by_category = dict(cursor.fetchall())
|
|
835
880
|
|
|
836
|
-
cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories')
|
|
881
|
+
cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories WHERE profile = ?', (active_profile,))
|
|
837
882
|
date_range = cursor.fetchone()
|
|
838
883
|
|
|
839
|
-
cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL')
|
|
884
|
+
cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL AND profile = ?', (active_profile,))
|
|
840
885
|
total_clusters = cursor.fetchone()[0]
|
|
841
886
|
|
|
842
|
-
cursor.execute('SELECT MAX(depth) FROM memories')
|
|
887
|
+
cursor.execute('SELECT MAX(depth) FROM memories WHERE profile = ?', (active_profile,))
|
|
843
888
|
max_depth = cursor.fetchone()[0] or 0
|
|
844
889
|
|
|
890
|
+
# Total across all profiles
|
|
891
|
+
cursor.execute('SELECT COUNT(*) FROM memories')
|
|
892
|
+
total_all_profiles = cursor.fetchone()[0]
|
|
893
|
+
|
|
845
894
|
conn.close()
|
|
846
895
|
|
|
847
896
|
return {
|
|
848
897
|
'total_memories': total_memories,
|
|
898
|
+
'total_all_profiles': total_all_profiles,
|
|
899
|
+
'active_profile': active_profile,
|
|
849
900
|
'total_projects': total_projects,
|
|
850
901
|
'total_clusters': total_clusters,
|
|
851
902
|
'max_tree_depth': max_depth,
|
package/src/pattern_learner.py
CHANGED
|
@@ -23,11 +23,14 @@ Based on architecture: docs/architecture/05-pattern-learner.md
|
|
|
23
23
|
import sqlite3
|
|
24
24
|
import json
|
|
25
25
|
import re
|
|
26
|
+
import logging
|
|
26
27
|
from datetime import datetime, timedelta
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import Dict, List, Optional, Any, Counter as CounterType
|
|
29
30
|
from collections import Counter
|
|
30
31
|
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
31
34
|
# Local NLP tools (no external APIs)
|
|
32
35
|
try:
|
|
33
36
|
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
@@ -404,23 +407,54 @@ class ConfidenceScorer:
|
|
|
404
407
|
evidence_memory_ids: List[int],
|
|
405
408
|
total_memories: int
|
|
406
409
|
) -> float:
|
|
407
|
-
"""
|
|
410
|
+
"""
|
|
411
|
+
Calculate confidence using Beta-Binomial Bayesian posterior.
|
|
412
|
+
|
|
413
|
+
Based on MACLA (arXiv:2512.18950, Forouzandeh et al., Dec 2025):
|
|
414
|
+
posterior_mean = (alpha + evidence) / (alpha + beta + evidence + competition)
|
|
415
|
+
|
|
416
|
+
Adaptation: MACLA's Beta-Binomial uses pairwise interaction counts.
|
|
417
|
+
Our corpus has sparse signals (most memories are irrelevant to any
|
|
418
|
+
single pattern). We use log-scaled competition instead of raw total
|
|
419
|
+
to avoid over-dilution: competition = log2(total_memories).
|
|
420
|
+
|
|
421
|
+
Pattern-specific priors (alpha, beta):
|
|
422
|
+
- preference (1, 4): prior mean 0.20, ~8 items to reach 0.5
|
|
423
|
+
- style (1, 5): prior mean 0.17, subtler signals need more evidence
|
|
424
|
+
- terminology (2, 3): prior mean 0.40, direct usage signal
|
|
425
|
+
"""
|
|
408
426
|
if total_memories == 0 or not evidence_memory_ids:
|
|
409
427
|
return 0.0
|
|
410
428
|
|
|
411
|
-
|
|
412
|
-
|
|
429
|
+
import math
|
|
430
|
+
evidence_count = len(evidence_memory_ids)
|
|
431
|
+
|
|
432
|
+
# Pattern-specific Beta priors (alpha, beta)
|
|
433
|
+
PRIORS = {
|
|
434
|
+
'preference': (1.0, 4.0),
|
|
435
|
+
'style': (1.0, 5.0),
|
|
436
|
+
'terminology': (2.0, 3.0),
|
|
437
|
+
}
|
|
438
|
+
alpha, beta = PRIORS.get(pattern_type, (1.0, 4.0))
|
|
439
|
+
|
|
440
|
+
# Log-scaled competition: grows slowly with corpus size
|
|
441
|
+
# 10 memories -> 3.3, 60 -> 5.9, 500 -> 9.0, 5000 -> 12.3
|
|
442
|
+
competition = math.log2(max(2, total_memories))
|
|
443
|
+
|
|
444
|
+
# MACLA-inspired Beta posterior with log competition
|
|
445
|
+
posterior_mean = (alpha + evidence_count) / (alpha + beta + evidence_count + competition)
|
|
413
446
|
|
|
414
|
-
#
|
|
447
|
+
# Recency adjustment (mild: 1.0 to 1.15)
|
|
415
448
|
recency_bonus = self._calculate_recency_bonus(evidence_memory_ids)
|
|
449
|
+
recency_factor = 1.0 + min(0.15, 0.075 * (recency_bonus - 1.0) / 0.2) if recency_bonus > 1.0 else 1.0
|
|
416
450
|
|
|
417
|
-
#
|
|
451
|
+
# Temporal spread adjustment (0.9 to 1.1)
|
|
418
452
|
distribution_factor = self._calculate_distribution_factor(evidence_memory_ids)
|
|
419
453
|
|
|
420
454
|
# Final confidence
|
|
421
|
-
confidence =
|
|
455
|
+
confidence = posterior_mean * recency_factor * distribution_factor
|
|
422
456
|
|
|
423
|
-
return min(
|
|
457
|
+
return min(0.95, round(confidence, 3))
|
|
424
458
|
|
|
425
459
|
def _calculate_recency_bonus(self, memory_ids: List[int]) -> float:
|
|
426
460
|
"""Give bonus to patterns with recent evidence."""
|
|
@@ -517,10 +551,21 @@ class PatternStore:
|
|
|
517
551
|
self._init_tables()
|
|
518
552
|
|
|
519
553
|
def _init_tables(self):
|
|
520
|
-
"""Initialize pattern tables if they don't exist."""
|
|
554
|
+
"""Initialize pattern tables if they don't exist, or recreate if schema is incomplete."""
|
|
521
555
|
conn = sqlite3.connect(self.db_path)
|
|
522
556
|
cursor = conn.cursor()
|
|
523
557
|
|
|
558
|
+
# Check if existing tables have correct schema
|
|
559
|
+
for table_name, required_cols in [
|
|
560
|
+
('identity_patterns', {'pattern_type', 'key', 'value', 'confidence'}),
|
|
561
|
+
('pattern_examples', {'pattern_id', 'memory_id'}),
|
|
562
|
+
]:
|
|
563
|
+
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
564
|
+
existing_cols = {row[1] for row in cursor.fetchall()}
|
|
565
|
+
if existing_cols and not required_cols.issubset(existing_cols):
|
|
566
|
+
logger.warning(f"Dropping incomplete {table_name} table (missing: {required_cols - existing_cols})")
|
|
567
|
+
cursor.execute(f'DROP TABLE IF EXISTS {table_name}')
|
|
568
|
+
|
|
524
569
|
# Identity patterns table
|
|
525
570
|
cursor.execute('''
|
|
526
571
|
CREATE TABLE IF NOT EXISTS identity_patterns (
|
|
@@ -532,12 +577,19 @@ class PatternStore:
|
|
|
532
577
|
evidence_count INTEGER DEFAULT 1,
|
|
533
578
|
memory_ids TEXT,
|
|
534
579
|
category TEXT,
|
|
580
|
+
profile TEXT DEFAULT 'default',
|
|
535
581
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
536
582
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
537
|
-
UNIQUE(pattern_type, key, category)
|
|
583
|
+
UNIQUE(pattern_type, key, category, profile)
|
|
538
584
|
)
|
|
539
585
|
''')
|
|
540
586
|
|
|
587
|
+
# Add profile column if upgrading from older schema
|
|
588
|
+
try:
|
|
589
|
+
cursor.execute('ALTER TABLE identity_patterns ADD COLUMN profile TEXT DEFAULT "default"')
|
|
590
|
+
except sqlite3.OperationalError:
|
|
591
|
+
pass # Column already exists
|
|
592
|
+
|
|
541
593
|
# Pattern examples table
|
|
542
594
|
cursor.execute('''
|
|
543
595
|
CREATE TABLE IF NOT EXISTS pattern_examples (
|
|
@@ -553,21 +605,23 @@ class PatternStore:
|
|
|
553
605
|
# Indexes
|
|
554
606
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_pattern_type ON identity_patterns(pattern_type)')
|
|
555
607
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_pattern_confidence ON identity_patterns(confidence)')
|
|
608
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_pattern_profile ON identity_patterns(profile)')
|
|
556
609
|
|
|
557
610
|
conn.commit()
|
|
558
611
|
conn.close()
|
|
559
612
|
|
|
560
613
|
def save_pattern(self, pattern: Dict[str, Any]) -> int:
|
|
561
|
-
"""Save or update a pattern."""
|
|
614
|
+
"""Save or update a pattern (scoped by profile)."""
|
|
562
615
|
conn = sqlite3.connect(self.db_path)
|
|
563
616
|
cursor = conn.cursor()
|
|
617
|
+
profile = pattern.get('profile', 'default')
|
|
564
618
|
|
|
565
619
|
try:
|
|
566
|
-
# Check if pattern exists
|
|
620
|
+
# Check if pattern exists for this profile
|
|
567
621
|
cursor.execute('''
|
|
568
622
|
SELECT id FROM identity_patterns
|
|
569
|
-
WHERE pattern_type = ? AND key = ? AND category = ?
|
|
570
|
-
''', (pattern['pattern_type'], pattern['key'], pattern['category']))
|
|
623
|
+
WHERE pattern_type = ? AND key = ? AND category = ? AND profile = ?
|
|
624
|
+
''', (pattern['pattern_type'], pattern['key'], pattern['category'], profile))
|
|
571
625
|
|
|
572
626
|
existing = cursor.fetchone()
|
|
573
627
|
|
|
@@ -592,8 +646,8 @@ class PatternStore:
|
|
|
592
646
|
# Insert new pattern
|
|
593
647
|
cursor.execute('''
|
|
594
648
|
INSERT INTO identity_patterns
|
|
595
|
-
(pattern_type, key, value, confidence, evidence_count, memory_ids, category)
|
|
596
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
649
|
+
(pattern_type, key, value, confidence, evidence_count, memory_ids, category, profile)
|
|
650
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
597
651
|
''', (
|
|
598
652
|
pattern['pattern_type'],
|
|
599
653
|
pattern['key'],
|
|
@@ -601,7 +655,8 @@ class PatternStore:
|
|
|
601
655
|
pattern['confidence'],
|
|
602
656
|
pattern['evidence_count'],
|
|
603
657
|
memory_ids_json,
|
|
604
|
-
pattern['category']
|
|
658
|
+
pattern['category'],
|
|
659
|
+
profile
|
|
605
660
|
))
|
|
606
661
|
pattern_id = cursor.lastrowid
|
|
607
662
|
|
|
@@ -648,25 +703,32 @@ class PatternStore:
|
|
|
648
703
|
# Fallback: first 150 chars
|
|
649
704
|
return content[:150] + ('...' if len(content) > 150 else '')
|
|
650
705
|
|
|
651
|
-
def get_patterns(self, min_confidence: float = 0.7, pattern_type: Optional[str] = None
|
|
652
|
-
|
|
706
|
+
def get_patterns(self, min_confidence: float = 0.7, pattern_type: Optional[str] = None,
|
|
707
|
+
profile: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
708
|
+
"""Get patterns above confidence threshold, optionally filtered by profile."""
|
|
653
709
|
conn = sqlite3.connect(self.db_path)
|
|
654
710
|
cursor = conn.cursor()
|
|
655
711
|
|
|
712
|
+
# Build query with optional filters
|
|
713
|
+
conditions = ['confidence >= ?']
|
|
714
|
+
params = [min_confidence]
|
|
715
|
+
|
|
656
716
|
if pattern_type:
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
717
|
+
conditions.append('pattern_type = ?')
|
|
718
|
+
params.append(pattern_type)
|
|
719
|
+
|
|
720
|
+
if profile:
|
|
721
|
+
conditions.append('profile = ?')
|
|
722
|
+
params.append(profile)
|
|
723
|
+
|
|
724
|
+
where_clause = ' AND '.join(conditions)
|
|
725
|
+
cursor.execute(f'''
|
|
726
|
+
SELECT id, pattern_type, key, value, confidence, evidence_count,
|
|
727
|
+
updated_at, created_at, category
|
|
728
|
+
FROM identity_patterns
|
|
729
|
+
WHERE {where_clause}
|
|
730
|
+
ORDER BY confidence DESC, evidence_count DESC
|
|
731
|
+
''', params)
|
|
670
732
|
|
|
671
733
|
patterns = []
|
|
672
734
|
for row in cursor.fetchall():
|
|
@@ -676,9 +738,11 @@ class PatternStore:
|
|
|
676
738
|
'key': row[2],
|
|
677
739
|
'value': row[3],
|
|
678
740
|
'confidence': row[4],
|
|
741
|
+
'evidence_count': row[5],
|
|
679
742
|
'frequency': row[5],
|
|
680
743
|
'last_seen': row[6],
|
|
681
|
-
'created_at': row[7]
|
|
744
|
+
'created_at': row[7],
|
|
745
|
+
'category': row[8]
|
|
682
746
|
})
|
|
683
747
|
|
|
684
748
|
conn.close()
|
|
@@ -696,23 +760,37 @@ class PatternLearner:
|
|
|
696
760
|
self.confidence_scorer = ConfidenceScorer(db_path)
|
|
697
761
|
self.pattern_store = PatternStore(db_path)
|
|
698
762
|
|
|
763
|
+
def _get_active_profile(self) -> str:
|
|
764
|
+
"""Get the currently active profile name from config."""
|
|
765
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
766
|
+
if config_file.exists():
|
|
767
|
+
try:
|
|
768
|
+
with open(config_file, 'r') as f:
|
|
769
|
+
config = json.load(f)
|
|
770
|
+
return config.get('active_profile', 'default')
|
|
771
|
+
except (json.JSONDecodeError, IOError):
|
|
772
|
+
pass
|
|
773
|
+
return 'default'
|
|
774
|
+
|
|
699
775
|
def weekly_pattern_update(self) -> Dict[str, int]:
|
|
700
|
-
"""Full pattern analysis of all memories. Run this weekly."""
|
|
701
|
-
|
|
776
|
+
"""Full pattern analysis of all memories for active profile. Run this weekly."""
|
|
777
|
+
active_profile = self._get_active_profile()
|
|
778
|
+
print(f"Starting weekly pattern update for profile: {active_profile}...")
|
|
702
779
|
|
|
703
|
-
# Get
|
|
780
|
+
# Get memory IDs for active profile only
|
|
704
781
|
conn = sqlite3.connect(self.db_path)
|
|
705
782
|
cursor = conn.cursor()
|
|
706
|
-
cursor.execute('SELECT id FROM memories ORDER BY created_at'
|
|
783
|
+
cursor.execute('SELECT id FROM memories WHERE profile = ? ORDER BY created_at',
|
|
784
|
+
(active_profile,))
|
|
707
785
|
all_memory_ids = [row[0] for row in cursor.fetchall()]
|
|
708
786
|
total_memories = len(all_memory_ids)
|
|
709
787
|
conn.close()
|
|
710
788
|
|
|
711
789
|
if total_memories == 0:
|
|
712
|
-
print("No memories found. Add memories first.")
|
|
790
|
+
print(f"No memories found for profile '{active_profile}'. Add memories first.")
|
|
713
791
|
return {'preferences': 0, 'styles': 0, 'terminology': 0}
|
|
714
792
|
|
|
715
|
-
print(f"Analyzing {total_memories} memories...")
|
|
793
|
+
print(f"Analyzing {total_memories} memories for profile '{active_profile}'...")
|
|
716
794
|
|
|
717
795
|
# Run all analyzers
|
|
718
796
|
preferences = self.frequency_analyzer.analyze_preferences(all_memory_ids)
|
|
@@ -724,7 +802,7 @@ class PatternLearner:
|
|
|
724
802
|
terms = self.terminology_learner.learn_terminology(all_memory_ids)
|
|
725
803
|
print(f" Found {len(terms)} terminology patterns")
|
|
726
804
|
|
|
727
|
-
# Recalculate confidence scores and save all patterns
|
|
805
|
+
# Recalculate confidence scores and save all patterns (tagged with profile)
|
|
728
806
|
counts = {'preferences': 0, 'styles': 0, 'terminology': 0}
|
|
729
807
|
|
|
730
808
|
for pattern in preferences.values():
|
|
@@ -736,6 +814,7 @@ class PatternLearner:
|
|
|
736
814
|
total_memories
|
|
737
815
|
)
|
|
738
816
|
pattern['confidence'] = round(confidence, 2)
|
|
817
|
+
pattern['profile'] = active_profile
|
|
739
818
|
self.pattern_store.save_pattern(pattern)
|
|
740
819
|
counts['preferences'] += 1
|
|
741
820
|
|
|
@@ -748,6 +827,7 @@ class PatternLearner:
|
|
|
748
827
|
total_memories
|
|
749
828
|
)
|
|
750
829
|
pattern['confidence'] = round(confidence, 2)
|
|
830
|
+
pattern['profile'] = active_profile
|
|
751
831
|
self.pattern_store.save_pattern(pattern)
|
|
752
832
|
counts['styles'] += 1
|
|
753
833
|
|
|
@@ -760,6 +840,7 @@ class PatternLearner:
|
|
|
760
840
|
total_memories
|
|
761
841
|
)
|
|
762
842
|
pattern['confidence'] = round(confidence, 2)
|
|
843
|
+
pattern['profile'] = active_profile
|
|
763
844
|
self.pattern_store.save_pattern(pattern)
|
|
764
845
|
counts['terminology'] += 1
|
|
765
846
|
|
|
@@ -772,11 +853,11 @@ class PatternLearner:
|
|
|
772
853
|
|
|
773
854
|
def on_new_memory(self, memory_id: int):
|
|
774
855
|
"""Incremental update when new memory is added."""
|
|
775
|
-
|
|
776
|
-
# Future optimization: only update affected patterns
|
|
856
|
+
active_profile = self._get_active_profile()
|
|
777
857
|
conn = sqlite3.connect(self.db_path)
|
|
778
858
|
cursor = conn.cursor()
|
|
779
|
-
cursor.execute('SELECT COUNT(*) FROM memories'
|
|
859
|
+
cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?',
|
|
860
|
+
(active_profile,))
|
|
780
861
|
total = cursor.fetchone()[0]
|
|
781
862
|
conn.close()
|
|
782
863
|
|
|
@@ -789,8 +870,9 @@ class PatternLearner:
|
|
|
789
870
|
self.weekly_pattern_update()
|
|
790
871
|
|
|
791
872
|
def get_patterns(self, min_confidence: float = 0.7) -> List[Dict[str, Any]]:
|
|
792
|
-
"""Query patterns above confidence threshold."""
|
|
793
|
-
|
|
873
|
+
"""Query patterns above confidence threshold for active profile."""
|
|
874
|
+
active_profile = self._get_active_profile()
|
|
875
|
+
return self.pattern_store.get_patterns(min_confidence, profile=active_profile)
|
|
794
876
|
|
|
795
877
|
def get_identity_context(self, min_confidence: float = 0.7) -> str:
|
|
796
878
|
"""Format patterns for Claude context injection."""
|
package/src/setup_validator.py
CHANGED
|
@@ -257,11 +257,18 @@ def initialize_database() -> Tuple[bool, str]:
|
|
|
257
257
|
CREATE TABLE IF NOT EXISTS graph_clusters (
|
|
258
258
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
259
259
|
cluster_name TEXT,
|
|
260
|
+
name TEXT,
|
|
260
261
|
description TEXT,
|
|
262
|
+
summary TEXT,
|
|
261
263
|
memory_count INTEGER DEFAULT 0,
|
|
264
|
+
member_count INTEGER DEFAULT 0,
|
|
262
265
|
avg_importance REAL DEFAULT 5.0,
|
|
263
266
|
top_entities TEXT DEFAULT '[]',
|
|
264
|
-
|
|
267
|
+
parent_cluster_id INTEGER,
|
|
268
|
+
depth INTEGER DEFAULT 0,
|
|
269
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
270
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
271
|
+
FOREIGN KEY (parent_cluster_id) REFERENCES graph_clusters(id) ON DELETE SET NULL
|
|
265
272
|
)
|
|
266
273
|
''')
|
|
267
274
|
|