superlocalmemory 2.4.2 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +62 -2
- package/docs/ARCHITECTURE-V2.5.md +190 -0
- package/docs/architecture-diagram.drawio +405 -0
- package/mcp_server.py +115 -14
- package/package.json +4 -1
- package/scripts/generate-thumbnails.py +220 -0
- 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/docs/COMPETITIVE-ANALYSIS.md +0 -210
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SuperLocalMemory V2 - Provenance Tracker
|
|
4
|
+
Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
5
|
+
Licensed under MIT License
|
|
6
|
+
|
|
7
|
+
Repository: https://github.com/varun369/SuperLocalMemoryV2
|
|
8
|
+
Author: Varun Pratap Bhardwaj (Solution Architect)
|
|
9
|
+
|
|
10
|
+
NOTICE: This software is protected by MIT License.
|
|
11
|
+
Attribution must be preserved in all copies or derivatives.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
ProvenanceTracker — Tracks the origin and lineage of every memory.
|
|
16
|
+
|
|
17
|
+
Adds provenance columns to the memories table:
|
|
18
|
+
created_by — Agent ID that created this memory (e.g., "mcp:claude-desktop")
|
|
19
|
+
source_protocol — Protocol used (mcp, cli, rest, python, a2a)
|
|
20
|
+
trust_score — Trust score at time of creation (default 1.0)
|
|
21
|
+
provenance_chain — JSON array of derivation history
|
|
22
|
+
|
|
23
|
+
This enables:
|
|
24
|
+
- "Who wrote this?" queries for the dashboard
|
|
25
|
+
- Trust-weighted recall (v2.6 — higher trust = higher ranking)
|
|
26
|
+
- Audit trail for enterprise compliance (v3.0)
|
|
27
|
+
- Memory lineage tracking (if agent B derives from agent A's memory)
|
|
28
|
+
|
|
29
|
+
Column migration is safe: uses ALTER TABLE ADD COLUMN with try/except.
|
|
30
|
+
Old databases without provenance columns work fine — values default.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import logging
|
|
35
|
+
import sqlite3
|
|
36
|
+
import threading
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Optional, Dict, Any
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger("superlocalmemory.provenance")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProvenanceTracker:
|
|
45
|
+
"""
|
|
46
|
+
Tracks provenance (origin) metadata for memories.
|
|
47
|
+
|
|
48
|
+
Singleton per database path. Thread-safe.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_instances: Dict[str, "ProvenanceTracker"] = {}
|
|
52
|
+
_instances_lock = threading.Lock()
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_instance(cls, db_path: Optional[Path] = None) -> "ProvenanceTracker":
|
|
56
|
+
"""Get or create the singleton ProvenanceTracker."""
|
|
57
|
+
if db_path is None:
|
|
58
|
+
db_path = Path.home() / ".claude-memory" / "memory.db"
|
|
59
|
+
key = str(db_path)
|
|
60
|
+
with cls._instances_lock:
|
|
61
|
+
if key not in cls._instances:
|
|
62
|
+
cls._instances[key] = cls(db_path)
|
|
63
|
+
return cls._instances[key]
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def reset_instance(cls, db_path: Optional[Path] = None):
|
|
67
|
+
"""Remove singleton. Used for testing."""
|
|
68
|
+
with cls._instances_lock:
|
|
69
|
+
if db_path is None:
|
|
70
|
+
cls._instances.clear()
|
|
71
|
+
else:
|
|
72
|
+
key = str(db_path)
|
|
73
|
+
if key in cls._instances:
|
|
74
|
+
del cls._instances[key]
|
|
75
|
+
|
|
76
|
+
def __init__(self, db_path: Path):
|
|
77
|
+
self.db_path = Path(db_path)
|
|
78
|
+
self._init_schema()
|
|
79
|
+
logger.info("ProvenanceTracker initialized: db=%s", self.db_path)
|
|
80
|
+
|
|
81
|
+
def _init_schema(self):
|
|
82
|
+
"""
|
|
83
|
+
Add provenance columns to memories table (safe migration).
|
|
84
|
+
|
|
85
|
+
Uses ALTER TABLE ADD COLUMN wrapped in try/except — safe for:
|
|
86
|
+
- Fresh databases (columns don't exist yet)
|
|
87
|
+
- Existing databases (columns might already exist)
|
|
88
|
+
- Concurrent migrations (OperationalError caught)
|
|
89
|
+
"""
|
|
90
|
+
provenance_columns = {
|
|
91
|
+
'created_by': "TEXT DEFAULT 'user'",
|
|
92
|
+
'source_protocol': "TEXT DEFAULT 'cli'",
|
|
93
|
+
'trust_score': "REAL DEFAULT 1.0",
|
|
94
|
+
'provenance_chain': "TEXT DEFAULT '[]'",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
from db_connection_manager import DbConnectionManager
|
|
99
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
100
|
+
|
|
101
|
+
def _migrate(conn):
|
|
102
|
+
cursor = conn.cursor()
|
|
103
|
+
|
|
104
|
+
# Check existing columns
|
|
105
|
+
cursor.execute("PRAGMA table_info(memories)")
|
|
106
|
+
existing = {row[1] for row in cursor.fetchall()}
|
|
107
|
+
|
|
108
|
+
for col_name, col_type in provenance_columns.items():
|
|
109
|
+
if col_name not in existing:
|
|
110
|
+
try:
|
|
111
|
+
cursor.execute(f"ALTER TABLE memories ADD COLUMN {col_name} {col_type}")
|
|
112
|
+
except sqlite3.OperationalError:
|
|
113
|
+
pass # Column already exists (concurrent migration)
|
|
114
|
+
|
|
115
|
+
# Index for provenance queries
|
|
116
|
+
try:
|
|
117
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_created_by ON memories(created_by)")
|
|
118
|
+
except sqlite3.OperationalError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_source_protocol ON memories(source_protocol)")
|
|
123
|
+
except sqlite3.OperationalError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
conn.commit()
|
|
127
|
+
|
|
128
|
+
mgr.execute_write(_migrate)
|
|
129
|
+
|
|
130
|
+
except ImportError:
|
|
131
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
132
|
+
cursor = conn.cursor()
|
|
133
|
+
|
|
134
|
+
cursor.execute("PRAGMA table_info(memories)")
|
|
135
|
+
existing = {row[1] for row in cursor.fetchall()}
|
|
136
|
+
|
|
137
|
+
for col_name, col_type in provenance_columns.items():
|
|
138
|
+
if col_name not in existing:
|
|
139
|
+
try:
|
|
140
|
+
cursor.execute(f"ALTER TABLE memories ADD COLUMN {col_name} {col_type}")
|
|
141
|
+
except sqlite3.OperationalError:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_created_by ON memories(created_by)")
|
|
146
|
+
except sqlite3.OperationalError:
|
|
147
|
+
pass
|
|
148
|
+
try:
|
|
149
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_source_protocol ON memories(source_protocol)")
|
|
150
|
+
except sqlite3.OperationalError:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
conn.commit()
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
# =========================================================================
|
|
157
|
+
# Record Provenance
|
|
158
|
+
# =========================================================================
|
|
159
|
+
|
|
160
|
+
def record_provenance(
|
|
161
|
+
self,
|
|
162
|
+
memory_id: int,
|
|
163
|
+
created_by: str = "user",
|
|
164
|
+
source_protocol: str = "cli",
|
|
165
|
+
trust_score: float = 1.0,
|
|
166
|
+
derived_from: Optional[int] = None,
|
|
167
|
+
):
|
|
168
|
+
"""
|
|
169
|
+
Record provenance metadata for a memory.
|
|
170
|
+
|
|
171
|
+
Called after a memory is created. Updates the provenance columns
|
|
172
|
+
on the memories table row.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
memory_id: ID of the memory to annotate
|
|
176
|
+
created_by: Agent ID that created this memory
|
|
177
|
+
source_protocol: Protocol used (mcp, cli, rest, python, a2a)
|
|
178
|
+
trust_score: Trust score at time of creation
|
|
179
|
+
derived_from: If this memory was derived from another, its ID
|
|
180
|
+
"""
|
|
181
|
+
trust_score = max(0.0, min(1.0, trust_score))
|
|
182
|
+
chain = json.dumps([derived_from] if derived_from else [])
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
from db_connection_manager import DbConnectionManager
|
|
186
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
187
|
+
|
|
188
|
+
def _update(conn):
|
|
189
|
+
conn.execute('''
|
|
190
|
+
UPDATE memories
|
|
191
|
+
SET created_by = ?, source_protocol = ?,
|
|
192
|
+
trust_score = ?, provenance_chain = ?
|
|
193
|
+
WHERE id = ?
|
|
194
|
+
''', (created_by, source_protocol, trust_score, chain, memory_id))
|
|
195
|
+
conn.commit()
|
|
196
|
+
|
|
197
|
+
mgr.execute_write(_update)
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
# Provenance failure must never break core operations
|
|
201
|
+
logger.error("Failed to record provenance for memory %d: %s", memory_id, e)
|
|
202
|
+
|
|
203
|
+
# =========================================================================
|
|
204
|
+
# Query Provenance
|
|
205
|
+
# =========================================================================
|
|
206
|
+
|
|
207
|
+
def get_provenance(self, memory_id: int) -> Optional[dict]:
|
|
208
|
+
"""
|
|
209
|
+
Get provenance metadata for a specific memory.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
memory_id: Memory ID to query
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dict with created_by, source_protocol, trust_score, provenance_chain
|
|
216
|
+
or None if memory not found
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
from db_connection_manager import DbConnectionManager
|
|
220
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
221
|
+
|
|
222
|
+
with mgr.read_connection() as conn:
|
|
223
|
+
cursor = conn.cursor()
|
|
224
|
+
cursor.execute("""
|
|
225
|
+
SELECT id, created_by, source_protocol, trust_score, provenance_chain
|
|
226
|
+
FROM memories WHERE id = ?
|
|
227
|
+
""", (memory_id,))
|
|
228
|
+
row = cursor.fetchone()
|
|
229
|
+
|
|
230
|
+
if not row:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
chain = []
|
|
234
|
+
try:
|
|
235
|
+
chain = json.loads(row[4]) if row[4] else []
|
|
236
|
+
except (json.JSONDecodeError, TypeError):
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"memory_id": row[0],
|
|
241
|
+
"created_by": row[1] or "user",
|
|
242
|
+
"source_protocol": row[2] or "cli",
|
|
243
|
+
"trust_score": row[3] if row[3] is not None else 1.0,
|
|
244
|
+
"provenance_chain": chain,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error("Failed to get provenance for memory %d: %s", memory_id, e)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def get_memories_by_agent(self, agent_id: str, limit: int = 50) -> list:
|
|
252
|
+
"""
|
|
253
|
+
Get all memories created by a specific agent.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
agent_id: Agent ID to query
|
|
257
|
+
limit: Max results
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of (memory_id, created_at, trust_score) tuples
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
from db_connection_manager import DbConnectionManager
|
|
264
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
265
|
+
|
|
266
|
+
with mgr.read_connection() as conn:
|
|
267
|
+
cursor = conn.cursor()
|
|
268
|
+
cursor.execute("""
|
|
269
|
+
SELECT id, created_at, trust_score
|
|
270
|
+
FROM memories
|
|
271
|
+
WHERE created_by = ?
|
|
272
|
+
ORDER BY created_at DESC
|
|
273
|
+
LIMIT ?
|
|
274
|
+
""", (agent_id, limit))
|
|
275
|
+
return [
|
|
276
|
+
{"memory_id": r[0], "created_at": r[1], "trust_score": r[2]}
|
|
277
|
+
for r in cursor.fetchall()
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.error("Failed to get memories by agent %s: %s", agent_id, e)
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
def get_provenance_stats(self) -> dict:
|
|
285
|
+
"""Get provenance statistics across all memories."""
|
|
286
|
+
try:
|
|
287
|
+
from db_connection_manager import DbConnectionManager
|
|
288
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
289
|
+
|
|
290
|
+
with mgr.read_connection() as conn:
|
|
291
|
+
cursor = conn.cursor()
|
|
292
|
+
|
|
293
|
+
cursor.execute("""
|
|
294
|
+
SELECT created_by, COUNT(*) as count
|
|
295
|
+
FROM memories
|
|
296
|
+
WHERE created_by IS NOT NULL
|
|
297
|
+
GROUP BY created_by
|
|
298
|
+
ORDER BY count DESC
|
|
299
|
+
""")
|
|
300
|
+
by_agent = dict(cursor.fetchall())
|
|
301
|
+
|
|
302
|
+
cursor.execute("""
|
|
303
|
+
SELECT source_protocol, COUNT(*) as count
|
|
304
|
+
FROM memories
|
|
305
|
+
WHERE source_protocol IS NOT NULL
|
|
306
|
+
GROUP BY source_protocol
|
|
307
|
+
ORDER BY count DESC
|
|
308
|
+
""")
|
|
309
|
+
by_protocol = dict(cursor.fetchall())
|
|
310
|
+
|
|
311
|
+
cursor.execute("SELECT AVG(trust_score) FROM memories WHERE trust_score IS NOT NULL")
|
|
312
|
+
avg_trust = cursor.fetchone()[0]
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"by_agent": by_agent,
|
|
316
|
+
"by_protocol": by_protocol,
|
|
317
|
+
"avg_trust_score": round(avg_trust, 3) if avg_trust else 1.0,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.error("Failed to get provenance stats: %s", e)
|
|
322
|
+
return {"by_agent": {}, "by_protocol": {}, "error": str(e)}
|