superlocalmemory 2.4.2 → 2.5.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.
@@ -26,6 +26,36 @@ import hashlib
26
26
  from datetime import datetime
27
27
  from pathlib import Path
28
28
  from typing import Optional, List, Dict, Any, Tuple
29
+ from contextlib import contextmanager
30
+
31
+ # Connection Manager (v2.5+) — fixes "database is locked" with multiple agents
32
+ try:
33
+ from db_connection_manager import DbConnectionManager
34
+ USE_CONNECTION_MANAGER = True
35
+ except ImportError:
36
+ USE_CONNECTION_MANAGER = False
37
+
38
+ # Event Bus (v2.5+) — real-time event broadcasting
39
+ try:
40
+ from event_bus import EventBus
41
+ USE_EVENT_BUS = True
42
+ except ImportError:
43
+ USE_EVENT_BUS = False
44
+
45
+ # Agent Registry + Provenance (v2.5+) — tracks who writes what
46
+ try:
47
+ from agent_registry import AgentRegistry
48
+ from provenance_tracker import ProvenanceTracker
49
+ USE_PROVENANCE = True
50
+ except ImportError:
51
+ USE_PROVENANCE = False
52
+
53
+ # Trust Scorer (v2.5+) — silent signal collection, no enforcement
54
+ try:
55
+ from trust_scorer import TrustScorer
56
+ USE_TRUST = True
57
+ except ImportError:
58
+ USE_TRUST = False
29
59
 
30
60
  # TF-IDF for local semantic search (no external APIs)
31
61
  try:
@@ -64,12 +94,116 @@ class MemoryStoreV2:
64
94
  self.db_path = db_path or DB_PATH
65
95
  self.vectors_path = VECTORS_PATH
66
96
  self._profile_override = profile
97
+
98
+ # Connection Manager (v2.5+) — thread-safe WAL + write queue
99
+ # Falls back to direct sqlite3.connect() if unavailable
100
+ self._db_mgr = None
101
+ if USE_CONNECTION_MANAGER:
102
+ try:
103
+ self._db_mgr = DbConnectionManager.get_instance(self.db_path)
104
+ except Exception:
105
+ pass # Fall back to direct connections
106
+
107
+ # Event Bus (v2.5+) — real-time event broadcasting
108
+ # If unavailable, events simply don't fire (core ops unaffected)
109
+ self._event_bus = None
110
+ if USE_EVENT_BUS:
111
+ try:
112
+ self._event_bus = EventBus.get_instance(self.db_path)
113
+ except Exception:
114
+ pass
115
+
67
116
  self._init_db()
117
+
118
+ # Agent Registry + Provenance (v2.5+)
119
+ # MUST run AFTER _init_db() — ProvenanceTracker ALTER TABLEs the memories table
120
+ self._agent_registry = None
121
+ self._provenance_tracker = None
122
+ if USE_PROVENANCE:
123
+ try:
124
+ self._agent_registry = AgentRegistry.get_instance(self.db_path)
125
+ self._provenance_tracker = ProvenanceTracker.get_instance(self.db_path)
126
+ except Exception:
127
+ pass
128
+
129
+ # Trust Scorer (v2.5+) — silent signal collection
130
+ self._trust_scorer = None
131
+ if USE_TRUST:
132
+ try:
133
+ self._trust_scorer = TrustScorer.get_instance(self.db_path)
134
+ except Exception:
135
+ pass
136
+
68
137
  self.vectorizer = None
69
138
  self.vectors = None
70
139
  self.memory_ids = []
71
140
  self._load_vectors()
72
141
 
142
+ # =========================================================================
143
+ # Connection helpers — abstract ConnectionManager vs direct sqlite3
144
+ # =========================================================================
145
+
146
+ @contextmanager
147
+ def _read_connection(self):
148
+ """
149
+ Context manager for read operations.
150
+ Uses ConnectionManager pool if available, else direct sqlite3.connect().
151
+ """
152
+ if self._db_mgr:
153
+ with self._db_mgr.read_connection() as conn:
154
+ yield conn
155
+ else:
156
+ conn = sqlite3.connect(self.db_path)
157
+ try:
158
+ yield conn
159
+ finally:
160
+ conn.close()
161
+
162
+ def _execute_write(self, callback):
163
+ """
164
+ Execute a write operation (INSERT/UPDATE/DELETE).
165
+ Uses ConnectionManager write queue if available, else direct sqlite3.connect().
166
+
167
+ Args:
168
+ callback: Function(conn) that performs writes and calls conn.commit()
169
+
170
+ Returns:
171
+ Whatever the callback returns
172
+ """
173
+ if self._db_mgr:
174
+ return self._db_mgr.execute_write(callback)
175
+ else:
176
+ conn = sqlite3.connect(self.db_path)
177
+ try:
178
+ result = callback(conn)
179
+ return result
180
+ finally:
181
+ conn.close()
182
+
183
+ def _emit_event(self, event_type: str, memory_id: Optional[int] = None, **kwargs):
184
+ """
185
+ Emit an event to the Event Bus (v2.5+).
186
+
187
+ Progressive enhancement: if Event Bus is unavailable, this is a no-op.
188
+ Event emission failure must NEVER break core memory operations.
189
+
190
+ Args:
191
+ event_type: Event type (e.g., "memory.created")
192
+ memory_id: Associated memory ID (if applicable)
193
+ **kwargs: Additional payload fields
194
+ """
195
+ if not self._event_bus:
196
+ return
197
+ try:
198
+ self._event_bus.emit(
199
+ event_type=event_type,
200
+ memory_id=memory_id,
201
+ payload=kwargs,
202
+ importance=kwargs.get("importance", 5),
203
+ )
204
+ except Exception:
205
+ pass # Event bus failure must never break core operations
206
+
73
207
  def _get_active_profile(self) -> str:
74
208
  """
75
209
  Get the currently active profile name.
@@ -90,173 +224,174 @@ class MemoryStoreV2:
90
224
 
91
225
  def _init_db(self):
92
226
  """Initialize SQLite database with V2 schema extensions."""
93
- conn = sqlite3.connect(self.db_path)
94
- cursor = conn.cursor()
95
-
96
- # Check if we need to add V2 columns to existing table
97
- cursor.execute("PRAGMA table_info(memories)")
98
- existing_columns = {row[1] for row in cursor.fetchall()}
99
-
100
- # Main memories table (V1 compatible + V2 extensions)
101
- cursor.execute('''
102
- CREATE TABLE IF NOT EXISTS memories (
103
- id INTEGER PRIMARY KEY AUTOINCREMENT,
104
- content TEXT NOT NULL,
105
- summary TEXT,
106
-
107
- -- Organization
108
- project_path TEXT,
109
- project_name TEXT,
110
- tags TEXT,
111
- category TEXT,
112
-
113
- -- Hierarchy (Layer 2 link)
114
- parent_id INTEGER,
115
- tree_path TEXT,
116
- depth INTEGER DEFAULT 0,
117
-
118
- -- Metadata
119
- memory_type TEXT DEFAULT 'session',
120
- importance INTEGER DEFAULT 5,
121
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
122
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
123
- last_accessed TIMESTAMP,
124
- access_count INTEGER DEFAULT 0,
125
-
126
- -- Deduplication
127
- content_hash TEXT UNIQUE,
128
-
129
- -- Graph (Layer 3 link)
130
- cluster_id INTEGER,
131
-
132
- FOREIGN KEY (parent_id) REFERENCES memories(id) ON DELETE CASCADE
133
- )
134
- ''')
135
-
136
- # Add missing V2 columns to existing table (migration support)
137
- # This handles upgrades from very old databases that might be missing columns
138
- v2_columns = {
139
- 'summary': 'TEXT',
140
- 'project_path': 'TEXT',
141
- 'project_name': 'TEXT',
142
- 'category': 'TEXT',
143
- 'parent_id': 'INTEGER',
144
- 'tree_path': 'TEXT',
145
- 'depth': 'INTEGER DEFAULT 0',
146
- 'memory_type': 'TEXT DEFAULT "session"',
147
- 'importance': 'INTEGER DEFAULT 5',
148
- 'updated_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
149
- 'last_accessed': 'TIMESTAMP',
150
- 'access_count': 'INTEGER DEFAULT 0',
151
- 'content_hash': 'TEXT',
152
- 'cluster_id': 'INTEGER',
153
- 'profile': 'TEXT DEFAULT "default"'
154
- }
227
+ def _do_init(conn):
228
+ cursor = conn.cursor()
229
+
230
+ # Check if we need to add V2 columns to existing table
231
+ cursor.execute("PRAGMA table_info(memories)")
232
+ existing_columns = {row[1] for row in cursor.fetchall()}
233
+
234
+ # Main memories table (V1 compatible + V2 extensions)
235
+ cursor.execute('''
236
+ CREATE TABLE IF NOT EXISTS memories (
237
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
238
+ content TEXT NOT NULL,
239
+ summary TEXT,
240
+
241
+ -- Organization
242
+ project_path TEXT,
243
+ project_name TEXT,
244
+ tags TEXT,
245
+ category TEXT,
246
+
247
+ -- Hierarchy (Layer 2 link)
248
+ parent_id INTEGER,
249
+ tree_path TEXT,
250
+ depth INTEGER DEFAULT 0,
251
+
252
+ -- Metadata
253
+ memory_type TEXT DEFAULT 'session',
254
+ importance INTEGER DEFAULT 5,
255
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
256
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
257
+ last_accessed TIMESTAMP,
258
+ access_count INTEGER DEFAULT 0,
259
+
260
+ -- Deduplication
261
+ content_hash TEXT UNIQUE,
262
+
263
+ -- Graph (Layer 3 link)
264
+ cluster_id INTEGER,
265
+
266
+ FOREIGN KEY (parent_id) REFERENCES memories(id) ON DELETE CASCADE
267
+ )
268
+ ''')
269
+
270
+ # Add missing V2 columns to existing table (migration support)
271
+ # This handles upgrades from very old databases that might be missing columns
272
+ v2_columns = {
273
+ 'summary': 'TEXT',
274
+ 'project_path': 'TEXT',
275
+ 'project_name': 'TEXT',
276
+ 'category': 'TEXT',
277
+ 'parent_id': 'INTEGER',
278
+ 'tree_path': 'TEXT',
279
+ 'depth': 'INTEGER DEFAULT 0',
280
+ 'memory_type': 'TEXT DEFAULT "session"',
281
+ 'importance': 'INTEGER DEFAULT 5',
282
+ 'updated_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
283
+ 'last_accessed': 'TIMESTAMP',
284
+ 'access_count': 'INTEGER DEFAULT 0',
285
+ 'content_hash': 'TEXT',
286
+ 'cluster_id': 'INTEGER',
287
+ 'profile': 'TEXT DEFAULT "default"'
288
+ }
289
+
290
+ for col_name, col_type in v2_columns.items():
291
+ if col_name not in existing_columns:
292
+ try:
293
+ cursor.execute(f'ALTER TABLE memories ADD COLUMN {col_name} {col_type}')
294
+ except sqlite3.OperationalError:
295
+ # Column might already exist from concurrent migration
296
+ pass
155
297
 
156
- for col_name, col_type in v2_columns.items():
157
- if col_name not in existing_columns:
298
+ # Sessions table (V1 compatible)
299
+ cursor.execute('''
300
+ CREATE TABLE IF NOT EXISTS sessions (
301
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
302
+ session_id TEXT UNIQUE,
303
+ project_path TEXT,
304
+ started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
305
+ ended_at TIMESTAMP,
306
+ summary TEXT
307
+ )
308
+ ''')
309
+
310
+ # Full-text search index (V1 compatible)
311
+ cursor.execute('''
312
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
313
+ USING fts5(content, summary, tags, content='memories', content_rowid='id')
314
+ ''')
315
+
316
+ # FTS Triggers (V1 compatible)
317
+ cursor.execute('''
318
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
319
+ INSERT INTO memories_fts(rowid, content, summary, tags)
320
+ VALUES (new.id, new.content, new.summary, new.tags);
321
+ END
322
+ ''')
323
+
324
+ cursor.execute('''
325
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
326
+ INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
327
+ VALUES('delete', old.id, old.content, old.summary, old.tags);
328
+ END
329
+ ''')
330
+
331
+ cursor.execute('''
332
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
333
+ INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
334
+ VALUES('delete', old.id, old.content, old.summary, old.tags);
335
+ INSERT INTO memories_fts(rowid, content, summary, tags)
336
+ VALUES (new.id, new.content, new.summary, new.tags);
337
+ END
338
+ ''')
339
+
340
+ # Create indexes for V2 fields (safe for old databases without V2 columns)
341
+ v2_indexes = [
342
+ ('idx_project', 'project_path'),
343
+ ('idx_tags', 'tags'),
344
+ ('idx_category', 'category'),
345
+ ('idx_tree_path', 'tree_path'),
346
+ ('idx_cluster', 'cluster_id'),
347
+ ('idx_last_accessed', 'last_accessed'),
348
+ ('idx_parent_id', 'parent_id'),
349
+ ('idx_profile', 'profile')
350
+ ]
351
+
352
+ for idx_name, col_name in v2_indexes:
158
353
  try:
159
- cursor.execute(f'ALTER TABLE memories ADD COLUMN {col_name} {col_type}')
354
+ cursor.execute(f'CREATE INDEX IF NOT EXISTS {idx_name} ON memories({col_name})')
160
355
  except sqlite3.OperationalError:
161
- # Column might already exist from concurrent migration
356
+ # Column doesn't exist yet (old database) - skip index creation
357
+ # Index will be created automatically on next schema upgrade
162
358
  pass
163
359
 
164
- # Sessions table (V1 compatible)
165
- cursor.execute('''
166
- CREATE TABLE IF NOT EXISTS sessions (
167
- id INTEGER PRIMARY KEY AUTOINCREMENT,
168
- session_id TEXT UNIQUE,
169
- project_path TEXT,
170
- started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
171
- ended_at TIMESTAMP,
172
- summary TEXT
173
- )
174
- ''')
175
-
176
- # Full-text search index (V1 compatible)
177
- cursor.execute('''
178
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
179
- USING fts5(content, summary, tags, content='memories', content_rowid='id')
180
- ''')
181
-
182
- # FTS Triggers (V1 compatible)
183
- cursor.execute('''
184
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
185
- INSERT INTO memories_fts(rowid, content, summary, tags)
186
- VALUES (new.id, new.content, new.summary, new.tags);
187
- END
188
- ''')
189
-
190
- cursor.execute('''
191
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
192
- INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
193
- VALUES('delete', old.id, old.content, old.summary, old.tags);
194
- END
195
- ''')
196
-
197
- cursor.execute('''
198
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
199
- INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
200
- VALUES('delete', old.id, old.content, old.summary, old.tags);
201
- INSERT INTO memories_fts(rowid, content, summary, tags)
202
- VALUES (new.id, new.content, new.summary, new.tags);
203
- END
204
- ''')
205
-
206
- # Create indexes for V2 fields (safe for old databases without V2 columns)
207
- v2_indexes = [
208
- ('idx_project', 'project_path'),
209
- ('idx_tags', 'tags'),
210
- ('idx_category', 'category'),
211
- ('idx_tree_path', 'tree_path'),
212
- ('idx_cluster', 'cluster_id'),
213
- ('idx_last_accessed', 'last_accessed'),
214
- ('idx_parent_id', 'parent_id'),
215
- ('idx_profile', 'profile')
216
- ]
217
-
218
- for idx_name, col_name in v2_indexes:
219
- try:
220
- cursor.execute(f'CREATE INDEX IF NOT EXISTS {idx_name} ON memories({col_name})')
221
- except sqlite3.OperationalError:
222
- # Column doesn't exist yet (old database) - skip index creation
223
- # Index will be created automatically on next schema upgrade
224
- pass
360
+ # Creator Attribution Metadata Table (REQUIRED by MIT License)
361
+ # This table embeds creator information directly in the database
362
+ cursor.execute('''
363
+ CREATE TABLE IF NOT EXISTS creator_metadata (
364
+ key TEXT PRIMARY KEY,
365
+ value TEXT NOT NULL,
366
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
367
+ )
368
+ ''')
225
369
 
226
- # Creator Attribution Metadata Table (REQUIRED by MIT License)
227
- # This table embeds creator information directly in the database
228
- cursor.execute('''
229
- CREATE TABLE IF NOT EXISTS creator_metadata (
230
- key TEXT PRIMARY KEY,
231
- value TEXT NOT NULL,
232
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
233
- )
234
- ''')
235
-
236
- # Insert creator attribution (embedded in database body)
237
- creator_data = {
238
- 'creator_name': 'Varun Pratap Bhardwaj',
239
- 'creator_role': 'Solution Architect & Original Creator',
240
- 'creator_github': 'varun369',
241
- 'project_name': 'SuperLocalMemory V2',
242
- 'project_url': 'https://github.com/varun369/SuperLocalMemoryV2',
243
- 'license': 'MIT',
244
- 'attribution_required': 'yes',
245
- 'version': '2.4.1',
246
- 'architecture_date': '2026-01-15',
247
- 'release_date': '2026-02-07',
248
- 'signature': 'VBPB-SLM-V2-2026-ARCHITECT',
249
- 'verification_hash': 'sha256:c9f3d1a8b5e2f4c6d8a9b3e7f1c4d6a8b9c3e7f2d5a8c1b4e6f9d2a7c5b8e1'
250
- }
370
+ # Insert creator attribution (embedded in database body)
371
+ creator_data = {
372
+ 'creator_name': 'Varun Pratap Bhardwaj',
373
+ 'creator_role': 'Solution Architect & Original Creator',
374
+ 'creator_github': 'varun369',
375
+ 'project_name': 'SuperLocalMemory V2',
376
+ 'project_url': 'https://github.com/varun369/SuperLocalMemoryV2',
377
+ 'license': 'MIT',
378
+ 'attribution_required': 'yes',
379
+ 'version': '2.5.0',
380
+ 'architecture_date': '2026-01-15',
381
+ 'release_date': '2026-02-07',
382
+ 'signature': 'VBPB-SLM-V2-2026-ARCHITECT',
383
+ 'verification_hash': 'sha256:c9f3d1a8b5e2f4c6d8a9b3e7f1c4d6a8b9c3e7f2d5a8c1b4e6f9d2a7c5b8e1'
384
+ }
251
385
 
252
- for key, value in creator_data.items():
253
- cursor.execute('''
254
- INSERT OR IGNORE INTO creator_metadata (key, value)
255
- VALUES (?, ?)
256
- ''', (key, value))
386
+ for key, value in creator_data.items():
387
+ cursor.execute('''
388
+ INSERT OR IGNORE INTO creator_metadata (key, value)
389
+ VALUES (?, ?)
390
+ ''', (key, value))
391
+
392
+ conn.commit()
257
393
 
258
- conn.commit()
259
- conn.close()
394
+ self._execute_write(_do_init)
260
395
 
261
396
  def _content_hash(self, content: str) -> str:
262
397
  """Generate hash for deduplication."""
@@ -327,70 +462,90 @@ class MemoryStoreV2:
327
462
  content_hash = self._content_hash(content)
328
463
  active_profile = self._get_active_profile()
329
464
 
330
- conn = sqlite3.connect(self.db_path)
331
- cursor = conn.cursor()
332
-
333
- try:
334
- # Calculate tree_path and depth
335
- tree_path, depth = self._calculate_tree_position(cursor, parent_id)
336
-
337
- cursor.execute('''
338
- INSERT INTO memories (
339
- content, summary, project_path, project_name, tags, category,
340
- parent_id, tree_path, depth,
341
- memory_type, importance, content_hash,
342
- last_accessed, access_count, profile
343
- )
344
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
345
- ''', (
346
- content,
347
- summary,
348
- project_path,
349
- project_name,
350
- json.dumps(tags) if tags else None,
351
- category,
352
- parent_id,
353
- tree_path,
354
- depth,
355
- memory_type,
356
- importance,
357
- content_hash,
358
- datetime.now().isoformat(),
359
- 0,
360
- active_profile
361
- ))
362
- memory_id = cursor.lastrowid
363
-
364
- # Update tree_path with actual memory_id
365
- if tree_path:
366
- tree_path = f"{tree_path}.{memory_id}"
367
- else:
368
- tree_path = str(memory_id)
465
+ def _do_add(conn):
466
+ cursor = conn.cursor()
369
467
 
370
- cursor.execute('UPDATE memories SET tree_path = ? WHERE id = ?', (tree_path, memory_id))
468
+ try:
469
+ # Calculate tree_path and depth
470
+ tree_path, depth = self._calculate_tree_position(cursor, parent_id)
471
+
472
+ cursor.execute('''
473
+ INSERT INTO memories (
474
+ content, summary, project_path, project_name, tags, category,
475
+ parent_id, tree_path, depth,
476
+ memory_type, importance, content_hash,
477
+ last_accessed, access_count, profile
478
+ )
479
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
480
+ ''', (
481
+ content,
482
+ summary,
483
+ project_path,
484
+ project_name,
485
+ json.dumps(tags) if tags else None,
486
+ category,
487
+ parent_id,
488
+ tree_path,
489
+ depth,
490
+ memory_type,
491
+ importance,
492
+ content_hash,
493
+ datetime.now().isoformat(),
494
+ 0,
495
+ active_profile
496
+ ))
497
+ memory_id = cursor.lastrowid
498
+
499
+ # Update tree_path with actual memory_id
500
+ if tree_path:
501
+ tree_path = f"{tree_path}.{memory_id}"
502
+ else:
503
+ tree_path = str(memory_id)
504
+
505
+ cursor.execute('UPDATE memories SET tree_path = ? WHERE id = ?', (tree_path, memory_id))
506
+
507
+ conn.commit()
508
+ return memory_id
509
+
510
+ except sqlite3.IntegrityError:
511
+ # Duplicate content
512
+ cursor.execute('SELECT id FROM memories WHERE content_hash = ?', (content_hash,))
513
+ result = cursor.fetchone()
514
+ return result[0] if result else -1
515
+
516
+ memory_id = self._execute_write(_do_add)
517
+
518
+ # Rebuild vectors after adding (reads only — outside write callback)
519
+ self._rebuild_vectors()
371
520
 
372
- conn.commit()
521
+ # Emit event (v2.5 — Event Bus)
522
+ self._emit_event("memory.created", memory_id=memory_id,
523
+ content_preview=content[:100], tags=tags,
524
+ project=project_name, importance=importance)
373
525
 
374
- # Rebuild vectors after adding
375
- self._rebuild_vectors()
526
+ # Record provenance (v2.5 — who created this memory)
527
+ if self._provenance_tracker:
528
+ try:
529
+ self._provenance_tracker.record_provenance(memory_id)
530
+ except Exception:
531
+ pass # Provenance failure must never break core
376
532
 
377
- # Auto-backup check (non-blocking)
533
+ # Trust signal (v2.5 — silent collection)
534
+ if self._trust_scorer:
378
535
  try:
379
- from auto_backup import AutoBackup
380
- backup = AutoBackup()
381
- backup.check_and_backup()
536
+ self._trust_scorer.on_memory_created("user", memory_id, importance)
382
537
  except Exception:
383
- pass # Backup failure must never break memory operations
538
+ pass # Trust failure must never break core
384
539
 
385
- return memory_id
540
+ # Auto-backup check (non-blocking)
541
+ try:
542
+ from auto_backup import AutoBackup
543
+ backup = AutoBackup()
544
+ backup.check_and_backup()
545
+ except Exception:
546
+ pass # Backup failure must never break memory operations
386
547
 
387
- except sqlite3.IntegrityError:
388
- # Duplicate content
389
- cursor.execute('SELECT id FROM memories WHERE content_hash = ?', (content_hash,))
390
- result = cursor.fetchone()
391
- return result[0] if result else -1
392
- finally:
393
- conn.close()
548
+ return memory_id
394
549
 
395
550
  def _calculate_tree_position(self, cursor: sqlite3.Cursor, parent_id: Optional[int]) -> Tuple[str, int]:
396
551
  """
@@ -444,69 +599,65 @@ class MemoryStoreV2:
444
599
  results = []
445
600
  active_profile = self._get_active_profile()
446
601
 
447
- # Method 1: TF-IDF semantic search
448
- if SKLEARN_AVAILABLE and self.vectorizer is not None and self.vectors is not None:
449
- try:
450
- query_vec = self.vectorizer.transform([query])
451
- similarities = cosine_similarity(query_vec, self.vectors).flatten()
452
- top_indices = np.argsort(similarities)[::-1][:limit * 2]
453
-
454
- conn = sqlite3.connect(self.db_path)
455
- cursor = conn.cursor()
456
-
457
- for idx in top_indices:
458
- if idx < len(self.memory_ids):
459
- memory_id = self.memory_ids[idx]
460
- score = float(similarities[idx])
461
-
462
- if score > 0.05: # Minimum relevance threshold
463
- cursor.execute('''
464
- SELECT id, content, summary, project_path, project_name, tags,
465
- category, parent_id, tree_path, depth,
466
- memory_type, importance, created_at, cluster_id,
467
- last_accessed, access_count
468
- FROM memories WHERE id = ? AND profile = ?
469
- ''', (memory_id, active_profile))
470
- row = cursor.fetchone()
471
-
472
- if row and self._apply_filters(row, project_path, memory_type,
473
- category, cluster_id, min_importance):
474
- results.append(self._row_to_dict(row, score, 'semantic'))
475
-
476
- conn.close()
477
- except Exception as e:
478
- print(f"Semantic search error: {e}")
479
-
480
- # Method 2: FTS fallback/supplement
481
- conn = sqlite3.connect(self.db_path)
482
- cursor = conn.cursor()
483
-
484
- # Clean query for FTS
485
- import re
486
- fts_query = ' OR '.join(re.findall(r'\w+', query))
487
-
488
- if fts_query:
489
- cursor.execute('''
490
- SELECT m.id, m.content, m.summary, m.project_path, m.project_name,
491
- m.tags, m.category, m.parent_id, m.tree_path, m.depth,
492
- m.memory_type, m.importance, m.created_at, m.cluster_id,
493
- m.last_accessed, m.access_count
494
- FROM memories m
495
- JOIN memories_fts fts ON m.id = fts.rowid
496
- WHERE memories_fts MATCH ? AND m.profile = ?
497
- ORDER BY rank
498
- LIMIT ?
499
- ''', (fts_query, active_profile, limit))
500
-
501
- existing_ids = {r['id'] for r in results}
502
-
503
- for row in cursor.fetchall():
504
- if row[0] not in existing_ids:
505
- if self._apply_filters(row, project_path, memory_type,
506
- category, cluster_id, min_importance):
507
- results.append(self._row_to_dict(row, 0.5, 'keyword'))
508
-
509
- conn.close()
602
+ with self._read_connection() as conn:
603
+ # Method 1: TF-IDF semantic search
604
+ if SKLEARN_AVAILABLE and self.vectorizer is not None and self.vectors is not None:
605
+ try:
606
+ query_vec = self.vectorizer.transform([query])
607
+ similarities = cosine_similarity(query_vec, self.vectors).flatten()
608
+ top_indices = np.argsort(similarities)[::-1][:limit * 2]
609
+
610
+ cursor = conn.cursor()
611
+
612
+ for idx in top_indices:
613
+ if idx < len(self.memory_ids):
614
+ memory_id = self.memory_ids[idx]
615
+ score = float(similarities[idx])
616
+
617
+ if score > 0.05: # Minimum relevance threshold
618
+ cursor.execute('''
619
+ SELECT id, content, summary, project_path, project_name, tags,
620
+ category, parent_id, tree_path, depth,
621
+ memory_type, importance, created_at, cluster_id,
622
+ last_accessed, access_count
623
+ FROM memories WHERE id = ? AND profile = ?
624
+ ''', (memory_id, active_profile))
625
+ row = cursor.fetchone()
626
+
627
+ if row and self._apply_filters(row, project_path, memory_type,
628
+ category, cluster_id, min_importance):
629
+ results.append(self._row_to_dict(row, score, 'semantic'))
630
+
631
+ except Exception as e:
632
+ print(f"Semantic search error: {e}")
633
+
634
+ # Method 2: FTS fallback/supplement
635
+ cursor = conn.cursor()
636
+
637
+ # Clean query for FTS
638
+ import re
639
+ fts_query = ' OR '.join(re.findall(r'\w+', query))
640
+
641
+ if fts_query:
642
+ cursor.execute('''
643
+ SELECT m.id, m.content, m.summary, m.project_path, m.project_name,
644
+ m.tags, m.category, m.parent_id, m.tree_path, m.depth,
645
+ m.memory_type, m.importance, m.created_at, m.cluster_id,
646
+ m.last_accessed, m.access_count
647
+ FROM memories m
648
+ JOIN memories_fts fts ON m.id = fts.rowid
649
+ WHERE memories_fts MATCH ? AND m.profile = ?
650
+ ORDER BY rank
651
+ LIMIT ?
652
+ ''', (fts_query, active_profile, limit))
653
+
654
+ existing_ids = {r['id'] for r in results}
655
+
656
+ for row in cursor.fetchall():
657
+ if row[0] not in existing_ids:
658
+ if self._apply_filters(row, project_path, memory_type,
659
+ category, cluster_id, min_importance):
660
+ results.append(self._row_to_dict(row, 0.5, 'keyword'))
510
661
 
511
662
  # Update access tracking for returned results
512
663
  self._update_access_tracking([r['id'] for r in results])
@@ -578,19 +729,18 @@ class MemoryStoreV2:
578
729
  if not memory_ids:
579
730
  return
580
731
 
581
- conn = sqlite3.connect(self.db_path)
582
- cursor = conn.cursor()
583
-
584
- now = datetime.now().isoformat()
585
- for mem_id in memory_ids:
586
- cursor.execute('''
587
- UPDATE memories
588
- SET last_accessed = ?, access_count = access_count + 1
589
- WHERE id = ?
590
- ''', (now, mem_id))
732
+ def _do_update(conn):
733
+ cursor = conn.cursor()
734
+ now = datetime.now().isoformat()
735
+ for mem_id in memory_ids:
736
+ cursor.execute('''
737
+ UPDATE memories
738
+ SET last_accessed = ?, access_count = access_count + 1
739
+ WHERE id = ?
740
+ ''', (now, mem_id))
741
+ conn.commit()
591
742
 
592
- conn.commit()
593
- conn.close()
743
+ self._execute_write(_do_update)
594
744
 
595
745
  def get_tree(self, parent_id: Optional[int] = None, max_depth: int = 3) -> List[Dict[str, Any]]:
596
746
  """
@@ -604,45 +754,44 @@ class MemoryStoreV2:
604
754
  List of memories with tree structure
605
755
  """
606
756
  active_profile = self._get_active_profile()
607
- conn = sqlite3.connect(self.db_path)
608
- cursor = conn.cursor()
609
-
610
- if parent_id is None:
611
- # Get root level memories
612
- cursor.execute('''
613
- SELECT id, content, summary, project_path, project_name, tags,
614
- category, parent_id, tree_path, depth, memory_type, importance,
615
- created_at, cluster_id, last_accessed, access_count
616
- FROM memories
617
- WHERE parent_id IS NULL AND depth <= ? AND profile = ?
618
- ORDER BY tree_path
619
- ''', (max_depth, active_profile))
620
- else:
621
- # Get subtree under specific parent
622
- cursor.execute('''
623
- SELECT tree_path FROM memories WHERE id = ?
624
- ''', (parent_id,))
625
- result = cursor.fetchone()
626
-
627
- if not result:
628
- conn.close()
629
- return []
630
757
 
631
- parent_path = result[0]
632
- cursor.execute('''
633
- SELECT id, content, summary, project_path, project_name, tags,
634
- category, parent_id, tree_path, depth, memory_type, importance,
635
- created_at, cluster_id, last_accessed, access_count
636
- FROM memories
637
- WHERE tree_path LIKE ? AND depth <= ?
638
- ORDER BY tree_path
639
- ''', (f"{parent_path}.%", max_depth))
640
-
641
- results = []
642
- for row in cursor.fetchall():
643
- results.append(self._row_to_dict(row, 1.0, 'tree'))
758
+ with self._read_connection() as conn:
759
+ cursor = conn.cursor()
760
+
761
+ if parent_id is None:
762
+ # Get root level memories
763
+ cursor.execute('''
764
+ SELECT id, content, summary, project_path, project_name, tags,
765
+ category, parent_id, tree_path, depth, memory_type, importance,
766
+ created_at, cluster_id, last_accessed, access_count
767
+ FROM memories
768
+ WHERE parent_id IS NULL AND depth <= ? AND profile = ?
769
+ ORDER BY tree_path
770
+ ''', (max_depth, active_profile))
771
+ else:
772
+ # Get subtree under specific parent
773
+ cursor.execute('''
774
+ SELECT tree_path FROM memories WHERE id = ?
775
+ ''', (parent_id,))
776
+ result = cursor.fetchone()
777
+
778
+ if not result:
779
+ return []
780
+
781
+ parent_path = result[0]
782
+ cursor.execute('''
783
+ SELECT id, content, summary, project_path, project_name, tags,
784
+ category, parent_id, tree_path, depth, memory_type, importance,
785
+ created_at, cluster_id, last_accessed, access_count
786
+ FROM memories
787
+ WHERE tree_path LIKE ? AND depth <= ?
788
+ ORDER BY tree_path
789
+ ''', (f"{parent_path}.%", max_depth))
790
+
791
+ results = []
792
+ for row in cursor.fetchall():
793
+ results.append(self._row_to_dict(row, 1.0, 'tree'))
644
794
 
645
- conn.close()
646
795
  return results
647
796
 
648
797
  def update_tier(self, memory_id: int, new_tier: str, compressed_summary: Optional[str] = None):
@@ -654,24 +803,26 @@ class MemoryStoreV2:
654
803
  new_tier: New tier level ('hot', 'warm', 'cold', 'archived')
655
804
  compressed_summary: Optional compressed summary for higher tiers
656
805
  """
657
- conn = sqlite3.connect(self.db_path)
658
- cursor = conn.cursor()
806
+ def _do_update(conn):
807
+ cursor = conn.cursor()
808
+ if compressed_summary:
809
+ cursor.execute('''
810
+ UPDATE memories
811
+ SET memory_type = ?, summary = ?, updated_at = ?
812
+ WHERE id = ?
813
+ ''', (new_tier, compressed_summary, datetime.now().isoformat(), memory_id))
814
+ else:
815
+ cursor.execute('''
816
+ UPDATE memories
817
+ SET memory_type = ?, updated_at = ?
818
+ WHERE id = ?
819
+ ''', (new_tier, datetime.now().isoformat(), memory_id))
820
+ conn.commit()
659
821
 
660
- if compressed_summary:
661
- cursor.execute('''
662
- UPDATE memories
663
- SET memory_type = ?, summary = ?, updated_at = ?
664
- WHERE id = ?
665
- ''', (new_tier, compressed_summary, datetime.now().isoformat(), memory_id))
666
- else:
667
- cursor.execute('''
668
- UPDATE memories
669
- SET memory_type = ?, updated_at = ?
670
- WHERE id = ?
671
- ''', (new_tier, datetime.now().isoformat(), memory_id))
822
+ self._execute_write(_do_update)
672
823
 
673
- conn.commit()
674
- conn.close()
824
+ # Emit event (v2.5)
825
+ self._emit_event("memory.updated", memory_id=memory_id, new_tier=new_tier)
675
826
 
676
827
  def get_by_cluster(self, cluster_id: int) -> List[Dict[str, Any]]:
677
828
  """
@@ -684,23 +835,23 @@ class MemoryStoreV2:
684
835
  List of memories in the cluster
685
836
  """
686
837
  active_profile = self._get_active_profile()
687
- conn = sqlite3.connect(self.db_path)
688
- cursor = conn.cursor()
689
-
690
- cursor.execute('''
691
- SELECT id, content, summary, project_path, project_name, tags,
692
- category, parent_id, tree_path, depth, memory_type, importance,
693
- created_at, cluster_id, last_accessed, access_count
694
- FROM memories
695
- WHERE cluster_id = ? AND profile = ?
696
- ORDER BY importance DESC, created_at DESC
697
- ''', (cluster_id, active_profile))
698
838
 
699
- results = []
700
- for row in cursor.fetchall():
701
- results.append(self._row_to_dict(row, 1.0, 'cluster'))
839
+ with self._read_connection() as conn:
840
+ cursor = conn.cursor()
841
+
842
+ cursor.execute('''
843
+ SELECT id, content, summary, project_path, project_name, tags,
844
+ category, parent_id, tree_path, depth, memory_type, importance,
845
+ created_at, cluster_id, last_accessed, access_count
846
+ FROM memories
847
+ WHERE cluster_id = ? AND profile = ?
848
+ ORDER BY importance DESC, created_at DESC
849
+ ''', (cluster_id, active_profile))
850
+
851
+ results = []
852
+ for row in cursor.fetchall():
853
+ results.append(self._row_to_dict(row, 1.0, 'cluster'))
702
854
 
703
- conn.close()
704
855
  return results
705
856
 
706
857
  # ========== V1 Backward Compatible Methods ==========
@@ -715,29 +866,28 @@ class MemoryStoreV2:
715
866
  return
716
867
 
717
868
  active_profile = self._get_active_profile()
718
- conn = sqlite3.connect(self.db_path)
719
- cursor = conn.cursor()
720
-
721
- # Check which columns exist (backward compatibility for old databases)
722
- cursor.execute("PRAGMA table_info(memories)")
723
- columns = {row[1] for row in cursor.fetchall()}
724
-
725
- # Build SELECT query based on available columns, filtered by profile
726
- has_profile = 'profile' in columns
727
- if 'summary' in columns:
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')
732
- rows = cursor.fetchall()
733
- texts = [f"{row[1]} {row[2] or ''}" for row in rows]
734
- else:
735
- # Old database without summary column
736
- cursor.execute('SELECT id, content FROM memories')
737
- rows = cursor.fetchall()
738
- texts = [row[1] for row in rows]
739
869
 
740
- conn.close()
870
+ with self._read_connection() as conn:
871
+ cursor = conn.cursor()
872
+
873
+ # Check which columns exist (backward compatibility for old databases)
874
+ cursor.execute("PRAGMA table_info(memories)")
875
+ columns = {row[1] for row in cursor.fetchall()}
876
+
877
+ # Build SELECT query based on available columns, filtered by profile
878
+ has_profile = 'profile' in columns
879
+ if 'summary' in columns:
880
+ if has_profile:
881
+ cursor.execute('SELECT id, content, summary FROM memories WHERE profile = ?', (active_profile,))
882
+ else:
883
+ cursor.execute('SELECT id, content, summary FROM memories')
884
+ rows = cursor.fetchall()
885
+ texts = [f"{row[1]} {row[2] or ''}" for row in rows]
886
+ else:
887
+ # Old database without summary column
888
+ cursor.execute('SELECT id, content FROM memories')
889
+ rows = cursor.fetchall()
890
+ texts = [row[1] for row in rows]
741
891
 
742
892
  if not rows:
743
893
  self.vectorizer = None
@@ -762,51 +912,50 @@ class MemoryStoreV2:
762
912
  def get_recent(self, limit: int = 10, project_path: Optional[str] = None) -> List[Dict[str, Any]]:
763
913
  """Get most recent memories (V1 compatible, profile-aware)."""
764
914
  active_profile = self._get_active_profile()
765
- conn = sqlite3.connect(self.db_path)
766
- cursor = conn.cursor()
767
915
 
768
- if project_path:
769
- cursor.execute('''
770
- SELECT id, content, summary, project_path, project_name, tags,
771
- category, parent_id, tree_path, depth, memory_type, importance,
772
- created_at, cluster_id, last_accessed, access_count
773
- FROM memories
774
- WHERE project_path = ? AND profile = ?
775
- ORDER BY created_at DESC
776
- LIMIT ?
777
- ''', (project_path, active_profile, limit))
778
- else:
779
- cursor.execute('''
780
- SELECT id, content, summary, project_path, project_name, tags,
781
- category, parent_id, tree_path, depth, memory_type, importance,
782
- created_at, cluster_id, last_accessed, access_count
783
- FROM memories
784
- WHERE profile = ?
785
- ORDER BY created_at DESC
786
- LIMIT ?
787
- ''', (active_profile, limit))
788
-
789
- results = []
790
- for row in cursor.fetchall():
791
- results.append(self._row_to_dict(row, 1.0, 'recent'))
916
+ with self._read_connection() as conn:
917
+ cursor = conn.cursor()
918
+
919
+ if project_path:
920
+ cursor.execute('''
921
+ SELECT id, content, summary, project_path, project_name, tags,
922
+ category, parent_id, tree_path, depth, memory_type, importance,
923
+ created_at, cluster_id, last_accessed, access_count
924
+ FROM memories
925
+ WHERE project_path = ? AND profile = ?
926
+ ORDER BY created_at DESC
927
+ LIMIT ?
928
+ ''', (project_path, active_profile, limit))
929
+ else:
930
+ cursor.execute('''
931
+ SELECT id, content, summary, project_path, project_name, tags,
932
+ category, parent_id, tree_path, depth, memory_type, importance,
933
+ created_at, cluster_id, last_accessed, access_count
934
+ FROM memories
935
+ WHERE profile = ?
936
+ ORDER BY created_at DESC
937
+ LIMIT ?
938
+ ''', (active_profile, limit))
939
+
940
+ results = []
941
+ for row in cursor.fetchall():
942
+ results.append(self._row_to_dict(row, 1.0, 'recent'))
792
943
 
793
- conn.close()
794
944
  return results
795
945
 
796
946
  def get_by_id(self, memory_id: int) -> Optional[Dict[str, Any]]:
797
947
  """Get a specific memory by ID (V1 compatible)."""
798
- conn = sqlite3.connect(self.db_path)
799
- cursor = conn.cursor()
948
+ with self._read_connection() as conn:
949
+ cursor = conn.cursor()
800
950
 
801
- cursor.execute('''
802
- SELECT id, content, summary, project_path, project_name, tags,
803
- category, parent_id, tree_path, depth, memory_type, importance,
804
- created_at, cluster_id, last_accessed, access_count
805
- FROM memories WHERE id = ?
806
- ''', (memory_id,))
951
+ cursor.execute('''
952
+ SELECT id, content, summary, project_path, project_name, tags,
953
+ category, parent_id, tree_path, depth, memory_type, importance,
954
+ created_at, cluster_id, last_accessed, access_count
955
+ FROM memories WHERE id = ?
956
+ ''', (memory_id,))
807
957
 
808
- row = cursor.fetchone()
809
- conn.close()
958
+ row = cursor.fetchone()
810
959
 
811
960
  if not row:
812
961
  return None
@@ -818,80 +967,89 @@ class MemoryStoreV2:
818
967
 
819
968
  def delete_memory(self, memory_id: int) -> bool:
820
969
  """Delete a specific memory (V1 compatible)."""
821
- conn = sqlite3.connect(self.db_path)
822
- cursor = conn.cursor()
823
- cursor.execute('DELETE FROM memories WHERE id = ?', (memory_id,))
824
- deleted = cursor.rowcount > 0
825
- conn.commit()
826
- conn.close()
970
+ def _do_delete(conn):
971
+ cursor = conn.cursor()
972
+ cursor.execute('DELETE FROM memories WHERE id = ?', (memory_id,))
973
+ deleted = cursor.rowcount > 0
974
+ conn.commit()
975
+ return deleted
976
+
977
+ deleted = self._execute_write(_do_delete)
827
978
 
828
979
  if deleted:
829
980
  self._rebuild_vectors()
981
+ # Emit event (v2.5)
982
+ self._emit_event("memory.deleted", memory_id=memory_id)
983
+ # Trust signal (v2.5 — silent)
984
+ if self._trust_scorer:
985
+ try:
986
+ self._trust_scorer.on_memory_deleted("user", memory_id)
987
+ except Exception:
988
+ pass
830
989
 
831
990
  return deleted
832
991
 
833
992
  def list_all(self, limit: int = 50) -> List[Dict[str, Any]]:
834
993
  """List all memories with short previews (V1 compatible, profile-aware)."""
835
994
  active_profile = self._get_active_profile()
836
- conn = sqlite3.connect(self.db_path)
837
- cursor = conn.cursor()
838
-
839
- cursor.execute('''
840
- SELECT id, content, summary, project_path, project_name, tags,
841
- category, parent_id, tree_path, depth, memory_type, importance,
842
- created_at, cluster_id, last_accessed, access_count
843
- FROM memories
844
- WHERE profile = ?
845
- ORDER BY created_at DESC
846
- LIMIT ?
847
- ''', (active_profile, limit))
848
995
 
849
- results = []
850
- for row in cursor.fetchall():
851
- mem_dict = self._row_to_dict(row, 1.0, 'list')
996
+ with self._read_connection() as conn:
997
+ cursor = conn.cursor()
998
+
999
+ cursor.execute('''
1000
+ SELECT id, content, summary, project_path, project_name, tags,
1001
+ category, parent_id, tree_path, depth, memory_type, importance,
1002
+ created_at, cluster_id, last_accessed, access_count
1003
+ FROM memories
1004
+ WHERE profile = ?
1005
+ ORDER BY created_at DESC
1006
+ LIMIT ?
1007
+ ''', (active_profile, limit))
1008
+
1009
+ results = []
1010
+ for row in cursor.fetchall():
1011
+ mem_dict = self._row_to_dict(row, 1.0, 'list')
852
1012
 
853
- # Add title field for V1 compatibility
854
- content = row[1]
855
- first_line = content.split('\n')[0][:60]
856
- mem_dict['title'] = first_line + ('...' if len(content) > 60 else '')
1013
+ # Add title field for V1 compatibility
1014
+ content = row[1]
1015
+ first_line = content.split('\n')[0][:60]
1016
+ mem_dict['title'] = first_line + ('...' if len(content) > 60 else '')
857
1017
 
858
- results.append(mem_dict)
1018
+ results.append(mem_dict)
859
1019
 
860
- conn.close()
861
1020
  return results
862
1021
 
863
1022
  def get_stats(self) -> Dict[str, Any]:
864
1023
  """Get memory store statistics (V1 compatible with V2 extensions, profile-aware)."""
865
1024
  active_profile = self._get_active_profile()
866
- conn = sqlite3.connect(self.db_path)
867
- cursor = conn.cursor()
868
1025
 
869
- cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?', (active_profile,))
870
- total_memories = cursor.fetchone()[0]
1026
+ with self._read_connection() as conn:
1027
+ cursor = conn.cursor()
871
1028
 
872
- cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL AND profile = ?', (active_profile,))
873
- total_projects = cursor.fetchone()[0]
1029
+ cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?', (active_profile,))
1030
+ total_memories = cursor.fetchone()[0]
874
1031
 
875
- cursor.execute('SELECT memory_type, COUNT(*) FROM memories WHERE profile = ? GROUP BY memory_type', (active_profile,))
876
- by_type = dict(cursor.fetchall())
1032
+ cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL AND profile = ?', (active_profile,))
1033
+ total_projects = cursor.fetchone()[0]
877
1034
 
878
- cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL AND profile = ? GROUP BY category', (active_profile,))
879
- by_category = dict(cursor.fetchall())
1035
+ cursor.execute('SELECT memory_type, COUNT(*) FROM memories WHERE profile = ? GROUP BY memory_type', (active_profile,))
1036
+ by_type = dict(cursor.fetchall())
880
1037
 
881
- cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories WHERE profile = ?', (active_profile,))
882
- date_range = cursor.fetchone()
1038
+ cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL AND profile = ? GROUP BY category', (active_profile,))
1039
+ by_category = dict(cursor.fetchall())
883
1040
 
884
- cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL AND profile = ?', (active_profile,))
885
- total_clusters = cursor.fetchone()[0]
1041
+ cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories WHERE profile = ?', (active_profile,))
1042
+ date_range = cursor.fetchone()
886
1043
 
887
- cursor.execute('SELECT MAX(depth) FROM memories WHERE profile = ?', (active_profile,))
888
- max_depth = cursor.fetchone()[0] or 0
1044
+ cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL AND profile = ?', (active_profile,))
1045
+ total_clusters = cursor.fetchone()[0]
889
1046
 
890
- # Total across all profiles
891
- cursor.execute('SELECT COUNT(*) FROM memories')
892
- total_all_profiles = cursor.fetchone()[0]
1047
+ cursor.execute('SELECT MAX(depth) FROM memories WHERE profile = ?', (active_profile,))
1048
+ max_depth = cursor.fetchone()[0] or 0
893
1049
 
894
- conn.close()
1050
+ # Total across all profiles
1051
+ cursor.execute('SELECT COUNT(*) FROM memories')
1052
+ total_all_profiles = cursor.fetchone()[0]
895
1053
 
896
1054
  return {
897
1055
  'total_memories': total_memories,
@@ -916,13 +1074,10 @@ class MemoryStoreV2:
916
1074
  Returns:
917
1075
  Dictionary with creator information and attribution requirements
918
1076
  """
919
- conn = sqlite3.connect(self.db_path)
920
- cursor = conn.cursor()
921
-
922
- cursor.execute('SELECT key, value FROM creator_metadata')
923
- attribution = dict(cursor.fetchall())
924
-
925
- conn.close()
1077
+ with self._read_connection() as conn:
1078
+ cursor = conn.cursor()
1079
+ cursor.execute('SELECT key, value FROM creator_metadata')
1080
+ attribution = dict(cursor.fetchall())
926
1081
 
927
1082
  # Fallback if table doesn't exist yet (old databases)
928
1083
  if not attribution: