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.
@@ -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)}