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.
- package/CHANGELOG.md +46 -0
- package/README.md +21 -0
- package/docs/ARCHITECTURE-V2.5.md +190 -0
- package/mcp_server.py +115 -14
- package/package.json +1 -1
- package/src/agent_registry.py +385 -0
- package/src/db_connection_manager.py +532 -0
- package/src/event_bus.py +555 -0
- package/src/memory_store_v2.py +626 -471
- package/src/provenance_tracker.py +322 -0
- package/src/subscription_manager.py +399 -0
- package/src/trust_scorer.py +456 -0
- package/src/webhook_dispatcher.py +229 -0
- package/ui/app.js +425 -0
- package/ui/index.html +147 -1
- package/ui/js/agents.js +192 -0
- package/ui/js/clusters.js +80 -0
- package/ui/js/core.js +230 -0
- package/ui/js/events.js +178 -0
- package/ui/js/graph.js +32 -0
- package/ui/js/init.js +31 -0
- package/ui/js/memories.js +149 -0
- package/ui/js/modal.js +139 -0
- package/ui/js/patterns.js +93 -0
- package/ui/js/profiles.js +202 -0
- package/ui/js/search.js +59 -0
- package/ui/js/settings.js +167 -0
- package/ui/js/timeline.js +32 -0
- package/ui_server.py +69 -1665
package/src/memory_store_v2.py
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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'
|
|
354
|
+
cursor.execute(f'CREATE INDEX IF NOT EXISTS {idx_name} ON memories({col_name})')
|
|
160
355
|
except sqlite3.OperationalError:
|
|
161
|
-
# Column
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
533
|
+
# Trust signal (v2.5 — silent collection)
|
|
534
|
+
if self._trust_scorer:
|
|
378
535
|
try:
|
|
379
|
-
|
|
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 #
|
|
538
|
+
pass # Trust failure must never break core
|
|
384
539
|
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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
|
-
|
|
632
|
-
cursor.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
cursor.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
799
|
-
|
|
948
|
+
with self._read_connection() as conn:
|
|
949
|
+
cursor = conn.cursor()
|
|
800
950
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
870
|
-
|
|
1026
|
+
with self._read_connection() as conn:
|
|
1027
|
+
cursor = conn.cursor()
|
|
871
1028
|
|
|
872
|
-
|
|
873
|
-
|
|
1029
|
+
cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?', (active_profile,))
|
|
1030
|
+
total_memories = cursor.fetchone()[0]
|
|
874
1031
|
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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
|
-
|
|
885
|
-
|
|
1041
|
+
cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories WHERE profile = ?', (active_profile,))
|
|
1042
|
+
date_range = cursor.fetchone()
|
|
886
1043
|
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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:
|