superlocalmemory 2.8.2 → 2.8.5
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/ATTRIBUTION.md +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +7 -5
- package/api_server.py +5 -0
- package/bin/slm +35 -0
- package/bin/slm.bat +3 -3
- package/docs/SECURITY-QUICK-REFERENCE.md +214 -0
- package/install.ps1 +11 -11
- package/mcp_server.py +78 -10
- package/package.json +2 -2
- package/requirements-core.txt +16 -18
- package/requirements-learning.txt +8 -8
- package/requirements.txt +9 -7
- package/scripts/prepack.js +33 -0
- package/scripts/verify-v27.ps1 +301 -0
- package/src/agent_registry.py +32 -28
- package/src/auto_backup.py +12 -6
- package/src/cache_manager.py +2 -2
- package/src/compression/__init__.py +25 -0
- package/src/compression/cli.py +150 -0
- package/src/compression/cold_storage.py +217 -0
- package/src/compression/config.py +72 -0
- package/src/compression/orchestrator.py +133 -0
- package/src/compression/tier2_compressor.py +228 -0
- package/src/compression/tier3_compressor.py +153 -0
- package/src/compression/tier_classifier.py +148 -0
- package/src/db_connection_manager.py +5 -5
- package/src/event_bus.py +24 -22
- package/src/hnsw_index.py +3 -3
- package/src/learning/__init__.py +5 -4
- package/src/learning/adaptive_ranker.py +14 -265
- package/src/learning/bootstrap/__init__.py +69 -0
- package/src/learning/bootstrap/constants.py +93 -0
- package/src/learning/bootstrap/db_queries.py +316 -0
- package/src/learning/bootstrap/sampling.py +82 -0
- package/src/learning/bootstrap/text_utils.py +71 -0
- package/src/learning/cross_project_aggregator.py +58 -57
- package/src/learning/db/__init__.py +40 -0
- package/src/learning/db/constants.py +44 -0
- package/src/learning/db/schema.py +279 -0
- package/src/learning/learning_db.py +15 -234
- package/src/learning/ranking/__init__.py +33 -0
- package/src/learning/ranking/constants.py +84 -0
- package/src/learning/ranking/helpers.py +278 -0
- package/src/learning/source_quality_scorer.py +66 -65
- package/src/learning/synthetic_bootstrap.py +28 -310
- package/src/memory/__init__.py +36 -0
- package/src/memory/cli.py +205 -0
- package/src/memory/constants.py +39 -0
- package/src/memory/helpers.py +28 -0
- package/src/memory/schema.py +166 -0
- package/src/memory-profiles.py +94 -86
- package/src/memory-reset.py +187 -185
- package/src/memory_compression.py +2 -2
- package/src/memory_store_v2.py +44 -354
- package/src/migrate_v1_to_v2.py +11 -10
- package/src/patterns/analyzers.py +104 -100
- package/src/patterns/learner.py +17 -13
- package/src/patterns/scoring.py +25 -21
- package/src/patterns/store.py +40 -38
- package/src/patterns/terminology.py +53 -51
- package/src/provenance_tracker.py +2 -2
- package/src/qualixar_attribution.py +1 -1
- package/src/search/engine.py +16 -14
- package/src/search/index_loader.py +13 -11
- package/src/setup_validator.py +160 -158
- package/src/subscription_manager.py +20 -18
- package/src/tree/builder.py +66 -64
- package/src/tree/nodes.py +103 -97
- package/src/tree/queries.py +142 -137
- package/src/tree/schema.py +46 -42
- package/src/webhook_dispatcher.py +3 -3
- package/ui_server.py +7 -4
|
@@ -41,59 +41,61 @@ class FrequencyAnalyzer:
|
|
|
41
41
|
patterns = {}
|
|
42
42
|
|
|
43
43
|
conn = sqlite3.connect(self.db_path)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
44
|
+
try:
|
|
45
|
+
cursor = conn.cursor()
|
|
46
|
+
|
|
47
|
+
for category, keywords in self.tech_categories.items():
|
|
48
|
+
keyword_counts = Counter()
|
|
49
|
+
evidence_memories = {} # {keyword: [memory_ids]}
|
|
50
|
+
|
|
51
|
+
for memory_id in memory_ids:
|
|
52
|
+
cursor.execute('SELECT content FROM memories WHERE id = ?', (memory_id,))
|
|
53
|
+
row = cursor.fetchone()
|
|
54
|
+
|
|
55
|
+
if not row:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
content = row[0].lower()
|
|
59
|
+
|
|
60
|
+
for keyword in keywords:
|
|
61
|
+
# Count occurrences with word boundaries
|
|
62
|
+
pattern = r'\b' + re.escape(keyword.replace('.', r'\.')) + r'\b'
|
|
63
|
+
matches = re.findall(pattern, content, re.IGNORECASE)
|
|
64
|
+
count = len(matches)
|
|
65
|
+
|
|
66
|
+
if count > 0:
|
|
67
|
+
keyword_counts[keyword] += count
|
|
68
|
+
|
|
69
|
+
if keyword not in evidence_memories:
|
|
70
|
+
evidence_memories[keyword] = []
|
|
71
|
+
evidence_memories[keyword].append(memory_id)
|
|
72
|
+
|
|
73
|
+
# Determine preference (most mentioned)
|
|
74
|
+
if keyword_counts:
|
|
75
|
+
top_keyword = keyword_counts.most_common(1)[0][0]
|
|
76
|
+
total_mentions = sum(keyword_counts.values())
|
|
77
|
+
top_count = keyword_counts[top_keyword]
|
|
78
|
+
|
|
79
|
+
# Calculate confidence (% of mentions)
|
|
80
|
+
confidence = top_count / total_mentions if total_mentions > 0 else 0
|
|
81
|
+
|
|
82
|
+
# Only create pattern if confidence > 0.6 and at least 3 mentions
|
|
83
|
+
if confidence > 0.6 and top_count >= 3:
|
|
84
|
+
value = self._format_preference(top_keyword, keyword_counts)
|
|
85
|
+
evidence_list = list(set(evidence_memories.get(top_keyword, [])))
|
|
86
|
+
|
|
87
|
+
patterns[category] = {
|
|
88
|
+
'pattern_type': 'preference',
|
|
89
|
+
'key': category,
|
|
90
|
+
'value': value,
|
|
91
|
+
'confidence': round(confidence, 2),
|
|
92
|
+
'evidence_count': len(evidence_list),
|
|
93
|
+
'memory_ids': evidence_list,
|
|
94
|
+
'category': self._categorize_pattern(category)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
finally:
|
|
98
|
+
conn.close()
|
|
97
99
|
return patterns
|
|
98
100
|
|
|
99
101
|
def _format_preference(self, top_keyword: str, all_counts: Counter) -> str:
|
|
@@ -171,53 +173,55 @@ class ContextAnalyzer:
|
|
|
171
173
|
patterns = {}
|
|
172
174
|
|
|
173
175
|
conn = sqlite3.connect(self.db_path)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
176
|
+
try:
|
|
177
|
+
cursor = conn.cursor()
|
|
178
|
+
|
|
179
|
+
for pattern_key, indicators in self.style_indicators.items():
|
|
180
|
+
indicator_counts = Counter()
|
|
181
|
+
evidence_memories = {} # {style_type: [memory_ids]}
|
|
182
|
+
|
|
183
|
+
for memory_id in memory_ids:
|
|
184
|
+
cursor.execute('SELECT content FROM memories WHERE id = ?', (memory_id,))
|
|
185
|
+
row = cursor.fetchone()
|
|
186
|
+
|
|
187
|
+
if not row:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
content = row[0].lower()
|
|
191
|
+
|
|
192
|
+
for style_type, keywords in indicators.items():
|
|
193
|
+
for keyword in keywords:
|
|
194
|
+
if keyword in content:
|
|
195
|
+
indicator_counts[style_type] += 1
|
|
196
|
+
|
|
197
|
+
if style_type not in evidence_memories:
|
|
198
|
+
evidence_memories[style_type] = []
|
|
199
|
+
evidence_memories[style_type].append(memory_id)
|
|
200
|
+
|
|
201
|
+
# Determine dominant style
|
|
202
|
+
if indicator_counts:
|
|
203
|
+
top_style = indicator_counts.most_common(1)[0][0]
|
|
204
|
+
total = sum(indicator_counts.values())
|
|
205
|
+
top_count = indicator_counts[top_style]
|
|
206
|
+
confidence = top_count / total if total > 0 else 0
|
|
207
|
+
|
|
208
|
+
# Only create pattern if confidence > 0.65 and at least 3 mentions
|
|
209
|
+
if confidence > 0.65 and top_count >= 3:
|
|
210
|
+
value = self._format_style_value(pattern_key, top_style, indicator_counts)
|
|
211
|
+
evidence_list = list(set(evidence_memories.get(top_style, [])))
|
|
212
|
+
|
|
213
|
+
patterns[pattern_key] = {
|
|
214
|
+
'pattern_type': 'style',
|
|
215
|
+
'key': pattern_key,
|
|
216
|
+
'value': value,
|
|
217
|
+
'confidence': round(confidence, 2),
|
|
218
|
+
'evidence_count': len(evidence_list),
|
|
219
|
+
'memory_ids': evidence_list,
|
|
220
|
+
'category': 'general'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
finally:
|
|
224
|
+
conn.close()
|
|
221
225
|
return patterns
|
|
222
226
|
|
|
223
227
|
def _format_style_value(self, pattern_key: str, top_style: str, all_counts: Counter) -> str:
|
package/src/patterns/learner.py
CHANGED
|
@@ -66,12 +66,14 @@ class PatternLearner:
|
|
|
66
66
|
|
|
67
67
|
# Get memory IDs for active profile only
|
|
68
68
|
conn = sqlite3.connect(self.db_path)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
try:
|
|
70
|
+
cursor = conn.cursor()
|
|
71
|
+
cursor.execute('SELECT id FROM memories WHERE profile = ? ORDER BY created_at',
|
|
72
|
+
(active_profile,))
|
|
73
|
+
all_memory_ids = [row[0] for row in cursor.fetchall()]
|
|
74
|
+
total_memories = len(all_memory_ids)
|
|
75
|
+
finally:
|
|
76
|
+
conn.close()
|
|
75
77
|
|
|
76
78
|
if total_memories == 0:
|
|
77
79
|
print(f"No memories found for profile '{active_profile}'. Add memories first.")
|
|
@@ -142,16 +144,18 @@ class PatternLearner:
|
|
|
142
144
|
"""Incremental update when new memory is added."""
|
|
143
145
|
active_profile = self._get_active_profile()
|
|
144
146
|
conn = sqlite3.connect(self.db_path)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
try:
|
|
148
|
+
cursor = conn.cursor()
|
|
149
|
+
cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?',
|
|
150
|
+
(active_profile,))
|
|
151
|
+
total = cursor.fetchone()[0]
|
|
152
|
+
finally:
|
|
153
|
+
conn.close()
|
|
150
154
|
|
|
151
155
|
# Only do incremental updates if we have many memories (>50)
|
|
152
156
|
if total > 50:
|
|
153
|
-
#
|
|
154
|
-
|
|
157
|
+
# Deferred to batch update for efficiency (see weekly_pattern_update)
|
|
158
|
+
pass
|
|
155
159
|
else:
|
|
156
160
|
# For small memory counts, just do full update
|
|
157
161
|
self.weekly_pattern_update()
|
package/src/patterns/scoring.py
CHANGED
|
@@ -83,18 +83,20 @@ class ConfidenceScorer:
|
|
|
83
83
|
def _calculate_recency_bonus(self, memory_ids: List[int]) -> float:
|
|
84
84
|
"""Give bonus to patterns with recent evidence."""
|
|
85
85
|
conn = sqlite3.connect(self.db_path)
|
|
86
|
-
|
|
86
|
+
try:
|
|
87
|
+
cursor = conn.cursor()
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
# Get timestamps
|
|
90
|
+
placeholders = ','.join('?' * len(memory_ids))
|
|
91
|
+
cursor.execute(f'''
|
|
92
|
+
SELECT created_at FROM memories
|
|
93
|
+
WHERE id IN ({placeholders})
|
|
94
|
+
ORDER BY created_at DESC
|
|
95
|
+
''', memory_ids)
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
timestamps = cursor.fetchall()
|
|
98
|
+
finally:
|
|
99
|
+
conn.close()
|
|
98
100
|
|
|
99
101
|
if not timestamps:
|
|
100
102
|
return 1.0
|
|
@@ -124,17 +126,19 @@ class ConfidenceScorer:
|
|
|
124
126
|
return 0.8 # Penalize low sample size
|
|
125
127
|
|
|
126
128
|
conn = sqlite3.connect(self.db_path)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
try:
|
|
130
|
+
cursor = conn.cursor()
|
|
131
|
+
|
|
132
|
+
placeholders = ','.join('?' * len(memory_ids))
|
|
133
|
+
cursor.execute(f'''
|
|
134
|
+
SELECT created_at FROM memories
|
|
135
|
+
WHERE id IN ({placeholders})
|
|
136
|
+
ORDER BY created_at
|
|
137
|
+
''', memory_ids)
|
|
138
|
+
|
|
139
|
+
timestamps = [row[0] for row in cursor.fetchall()]
|
|
140
|
+
finally:
|
|
141
|
+
conn.close()
|
|
138
142
|
|
|
139
143
|
if len(timestamps) < 2:
|
|
140
144
|
return 0.8
|
package/src/patterns/store.py
CHANGED
|
@@ -181,43 +181,45 @@ class PatternStore:
|
|
|
181
181
|
profile: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
182
182
|
"""Get patterns above confidence threshold, optionally filtered by profile."""
|
|
183
183
|
conn = sqlite3.connect(self.db_path)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
184
|
+
try:
|
|
185
|
+
cursor = conn.cursor()
|
|
186
|
+
|
|
187
|
+
# Build query with optional filters
|
|
188
|
+
conditions = ['confidence >= ?']
|
|
189
|
+
params = [min_confidence]
|
|
190
|
+
|
|
191
|
+
if pattern_type:
|
|
192
|
+
conditions.append('pattern_type = ?')
|
|
193
|
+
params.append(pattern_type)
|
|
194
|
+
|
|
195
|
+
if profile:
|
|
196
|
+
conditions.append('profile = ?')
|
|
197
|
+
params.append(profile)
|
|
198
|
+
|
|
199
|
+
where_clause = ' AND '.join(conditions)
|
|
200
|
+
cursor.execute(f'''
|
|
201
|
+
SELECT id, pattern_type, key, value, confidence, evidence_count,
|
|
202
|
+
updated_at, created_at, category
|
|
203
|
+
FROM identity_patterns
|
|
204
|
+
WHERE {where_clause}
|
|
205
|
+
ORDER BY confidence DESC, evidence_count DESC
|
|
206
|
+
''', params)
|
|
207
|
+
|
|
208
|
+
patterns = []
|
|
209
|
+
for row in cursor.fetchall():
|
|
210
|
+
patterns.append({
|
|
211
|
+
'id': row[0],
|
|
212
|
+
'pattern_type': row[1],
|
|
213
|
+
'key': row[2],
|
|
214
|
+
'value': row[3],
|
|
215
|
+
'confidence': row[4],
|
|
216
|
+
'evidence_count': row[5],
|
|
217
|
+
'frequency': row[5],
|
|
218
|
+
'last_seen': row[6],
|
|
219
|
+
'created_at': row[7],
|
|
220
|
+
'category': row[8]
|
|
221
|
+
})
|
|
221
222
|
|
|
222
|
-
|
|
223
|
+
finally:
|
|
224
|
+
conn.close()
|
|
223
225
|
return patterns
|
|
@@ -36,57 +36,59 @@ class TerminologyLearner:
|
|
|
36
36
|
patterns = {}
|
|
37
37
|
|
|
38
38
|
conn = sqlite3.connect(self.db_path)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
39
|
+
try:
|
|
40
|
+
cursor = conn.cursor()
|
|
41
|
+
|
|
42
|
+
for term in self.ambiguous_terms:
|
|
43
|
+
contexts = []
|
|
44
|
+
|
|
45
|
+
# Find all contexts where term appears
|
|
46
|
+
for memory_id in memory_ids:
|
|
47
|
+
cursor.execute('SELECT content FROM memories WHERE id = ?', (memory_id,))
|
|
48
|
+
row = cursor.fetchone()
|
|
49
|
+
|
|
50
|
+
if not row:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
content = row[0]
|
|
54
|
+
|
|
55
|
+
# Find term in content (case-insensitive)
|
|
56
|
+
pattern = r'\b' + re.escape(term) + r'\b'
|
|
57
|
+
for match in re.finditer(pattern, content, re.IGNORECASE):
|
|
58
|
+
term_idx = match.start()
|
|
59
|
+
|
|
60
|
+
# Extract 100-char window around term
|
|
61
|
+
start = max(0, term_idx - 100)
|
|
62
|
+
end = min(len(content), term_idx + len(term) + 100)
|
|
63
|
+
context_window = content[start:end]
|
|
64
|
+
|
|
65
|
+
contexts.append({
|
|
66
|
+
'memory_id': memory_id,
|
|
67
|
+
'context': context_window
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
# Analyze contexts to extract meaning (need at least 3 examples)
|
|
71
|
+
if len(contexts) >= 3:
|
|
72
|
+
definition = self._extract_definition(term, contexts)
|
|
73
|
+
|
|
74
|
+
if definition:
|
|
75
|
+
evidence_list = list(set([ctx['memory_id'] for ctx in contexts]))
|
|
76
|
+
|
|
77
|
+
# Confidence increases with more examples, capped at 0.95
|
|
78
|
+
confidence = min(0.95, 0.6 + (len(contexts) * 0.05))
|
|
79
|
+
|
|
80
|
+
patterns[term] = {
|
|
81
|
+
'pattern_type': 'terminology',
|
|
82
|
+
'key': term,
|
|
83
|
+
'value': definition,
|
|
84
|
+
'confidence': round(confidence, 2),
|
|
85
|
+
'evidence_count': len(evidence_list),
|
|
86
|
+
'memory_ids': evidence_list,
|
|
87
|
+
'category': 'general'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
finally:
|
|
91
|
+
conn.close()
|
|
90
92
|
return patterns
|
|
91
93
|
|
|
92
94
|
def _extract_definition(self, term: str, contexts: List[Dict]) -> Optional[str]:
|
|
@@ -53,7 +53,7 @@ class ProvenanceTracker:
|
|
|
53
53
|
return cls._instances[key]
|
|
54
54
|
|
|
55
55
|
@classmethod
|
|
56
|
-
def reset_instance(cls, db_path: Optional[Path] = None):
|
|
56
|
+
def reset_instance(cls, db_path: Optional[Path] = None) -> None:
|
|
57
57
|
"""Remove singleton. Used for testing."""
|
|
58
58
|
with cls._instances_lock:
|
|
59
59
|
if db_path is None:
|
|
@@ -154,7 +154,7 @@ class ProvenanceTracker:
|
|
|
154
154
|
source_protocol: str = "cli",
|
|
155
155
|
trust_score: float = 1.0,
|
|
156
156
|
derived_from: Optional[int] = None,
|
|
157
|
-
):
|
|
157
|
+
) -> None:
|
|
158
158
|
"""
|
|
159
159
|
Record provenance metadata for a memory.
|
|
160
160
|
|
package/src/search/engine.py
CHANGED
|
@@ -172,20 +172,22 @@ class HybridSearchEngine(IndexLoaderMixin, SearchMethodsMixin, FusionMixin):
|
|
|
172
172
|
id_to_score = {mem_id: score for mem_id, score in raw_results}
|
|
173
173
|
|
|
174
174
|
conn = sqlite3.connect(self.db_path)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
175
|
+
try:
|
|
176
|
+
cursor = conn.cursor()
|
|
177
|
+
|
|
178
|
+
# Fetch memories
|
|
179
|
+
placeholders = ','.join(['?'] * len(memory_ids))
|
|
180
|
+
cursor.execute(f'''
|
|
181
|
+
SELECT id, content, summary, project_path, project_name, tags,
|
|
182
|
+
category, parent_id, tree_path, depth, memory_type,
|
|
183
|
+
importance, created_at, cluster_id, last_accessed, access_count
|
|
184
|
+
FROM memories
|
|
185
|
+
WHERE id IN ({placeholders})
|
|
186
|
+
''', memory_ids)
|
|
187
|
+
|
|
188
|
+
rows = cursor.fetchall()
|
|
189
|
+
finally:
|
|
190
|
+
conn.close()
|
|
189
191
|
|
|
190
192
|
# Build result dictionaries
|
|
191
193
|
results = []
|
|
@@ -31,17 +31,19 @@ class IndexLoaderMixin:
|
|
|
31
31
|
Load documents from database and build search indexes.
|
|
32
32
|
"""
|
|
33
33
|
conn = sqlite3.connect(self.db_path)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
try:
|
|
35
|
+
cursor = conn.cursor()
|
|
36
|
+
|
|
37
|
+
# Fetch all memories
|
|
38
|
+
cursor.execute('''
|
|
39
|
+
SELECT id, content, summary, tags
|
|
40
|
+
FROM memories
|
|
41
|
+
ORDER BY id
|
|
42
|
+
''')
|
|
43
|
+
|
|
44
|
+
rows = cursor.fetchall()
|
|
45
|
+
finally:
|
|
46
|
+
conn.close()
|
|
45
47
|
|
|
46
48
|
if not rows:
|
|
47
49
|
return
|