superlocalmemory 3.4.9 → 3.4.11

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.
Files changed (52) hide show
  1. package/README.md +23 -3
  2. package/docs/cloud-backup.md +174 -0
  3. package/docs/skill-evolution.md +256 -0
  4. package/ide/hooks/tool-event-hook.sh +101 -11
  5. package/package.json +1 -1
  6. package/pyproject.toml +3 -2
  7. package/src/superlocalmemory/cli/commands.py +359 -0
  8. package/src/superlocalmemory/cli/ingest_cmd.py +81 -29
  9. package/src/superlocalmemory/cli/main.py +32 -0
  10. package/src/superlocalmemory/cli/setup_wizard.py +54 -11
  11. package/src/superlocalmemory/core/config.py +35 -0
  12. package/src/superlocalmemory/core/consolidation_engine.py +138 -0
  13. package/src/superlocalmemory/core/embedding_worker.py +1 -1
  14. package/src/superlocalmemory/core/engine.py +19 -0
  15. package/src/superlocalmemory/core/fact_consolidator.py +425 -0
  16. package/src/superlocalmemory/core/graph_pruner.py +290 -0
  17. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -3
  18. package/src/superlocalmemory/core/recall_pipeline.py +9 -0
  19. package/src/superlocalmemory/core/tier_manager.py +325 -0
  20. package/src/superlocalmemory/encoding/entity_resolver.py +96 -28
  21. package/src/superlocalmemory/evolution/__init__.py +29 -0
  22. package/src/superlocalmemory/evolution/blind_verifier.py +115 -0
  23. package/src/superlocalmemory/evolution/evolution_store.py +302 -0
  24. package/src/superlocalmemory/evolution/mutation_generator.py +181 -0
  25. package/src/superlocalmemory/evolution/skill_evolver.py +555 -0
  26. package/src/superlocalmemory/evolution/triggers.py +367 -0
  27. package/src/superlocalmemory/evolution/types.py +92 -0
  28. package/src/superlocalmemory/hooks/hook_handlers.py +13 -0
  29. package/src/superlocalmemory/infra/backup.py +63 -20
  30. package/src/superlocalmemory/infra/cloud_backup.py +703 -0
  31. package/src/superlocalmemory/learning/skill_performance_miner.py +422 -0
  32. package/src/superlocalmemory/mcp/server.py +4 -0
  33. package/src/superlocalmemory/mcp/tools_evolution.py +338 -0
  34. package/src/superlocalmemory/retrieval/engine.py +64 -4
  35. package/src/superlocalmemory/retrieval/forgetting_filter.py +22 -7
  36. package/src/superlocalmemory/retrieval/strategy.py +2 -2
  37. package/src/superlocalmemory/server/routes/backup.py +512 -8
  38. package/src/superlocalmemory/server/routes/behavioral.py +39 -17
  39. package/src/superlocalmemory/server/routes/evolution.py +213 -0
  40. package/src/superlocalmemory/server/routes/tiers.py +195 -0
  41. package/src/superlocalmemory/server/unified_daemon.py +36 -5
  42. package/src/superlocalmemory/storage/schema_v3410.py +159 -0
  43. package/src/superlocalmemory/storage/schema_v3411.py +149 -0
  44. package/src/superlocalmemory/ui/index.html +59 -3
  45. package/src/superlocalmemory/ui/js/core.js +3 -0
  46. package/src/superlocalmemory/ui/js/lifecycle.js +83 -0
  47. package/src/superlocalmemory/ui/js/ng-entities.js +27 -3
  48. package/src/superlocalmemory/ui/js/ng-shell.js +33 -0
  49. package/src/superlocalmemory/ui/js/ng-skills.js +611 -0
  50. package/src/superlocalmemory/ui/js/settings.js +311 -1
  51. package/src/superlocalmemory.egg-info/PKG-INFO +16 -1
  52. package/src/superlocalmemory.egg-info/SOURCES.txt +18 -0
@@ -0,0 +1,422 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Skill Performance Miner — tracks per-skill effectiveness from tool events.
6
+
7
+ Zero-LLM approach: mines tool_events table for Skill tool invocations,
8
+ builds execution traces from surrounding events, computes approximate
9
+ outcome heuristics, and creates skill-level behavioral assertions.
10
+
11
+ Runs as Step 10 in the consolidation pipeline (after Step 9: soft prompts).
12
+ Depends on enriched tool_events (v3.4.10 hook with input_summary/output_summary).
13
+
14
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import json
21
+ import logging
22
+ import sqlite3
23
+ from collections import Counter, defaultdict
24
+ from datetime import datetime, timezone
25
+ from pathlib import Path
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Thresholds — conservative to avoid hallucinating patterns
30
+ MIN_INVOCATIONS = 5 # Don't create assertions for skills with fewer uses
31
+ MIN_CONFIDENCE = 0.5 # Don't inject into soft prompts below this
32
+ TRACE_WINDOW = 10 # Number of tool events to look at after a Skill call
33
+ RETRY_WINDOW_SECONDS = 300 # 5 minutes — same Skill re-invoked = potential retry
34
+ REINFORCEMENT_NUDGE = 0.10 # Bayesian confidence increase per consolidation cycle
35
+
36
+
37
+ class SkillPerformanceMiner:
38
+ """Mine tool_events for per-skill performance metrics.
39
+
40
+ Discovers patterns like:
41
+ - "brainstorming skill: 82% effective, 47 invocations, best for feature planning"
42
+ - "TDD + code-review used together: +23% effective vs individually"
43
+ - "brainstorm skill degraded: effective_rate dropped from 0.82 to 0.35"
44
+ """
45
+
46
+ def __init__(self, db_path: str | Path):
47
+ self._db_path = str(db_path)
48
+
49
+ def mine(self, profile_id: str = "default") -> dict:
50
+ """Run skill performance mining. Returns summary."""
51
+ result = {
52
+ "skills_found": 0,
53
+ "assertions_created": 0,
54
+ "assertions_reinforced": 0,
55
+ "entities_updated": 0,
56
+ }
57
+
58
+ conn = sqlite3.connect(self._db_path, timeout=10)
59
+ conn.row_factory = sqlite3.Row
60
+
61
+ try:
62
+ # Step 1: Find all Skill tool invocations
63
+ skill_events = self._get_skill_events(conn, profile_id)
64
+ if not skill_events:
65
+ return result
66
+
67
+ # Step 2: Extract skill names from input_summary
68
+ skill_invocations = self._parse_skill_invocations(skill_events)
69
+ result["skills_found"] = len(set(s["skill_name"] for s in skill_invocations))
70
+
71
+ if not skill_invocations:
72
+ return result
73
+
74
+ # Step 3: Build execution traces and compute outcomes
75
+ skill_metrics = self._compute_skill_metrics(
76
+ conn, profile_id, skill_invocations,
77
+ )
78
+
79
+ # Step 4: Create/update behavioral assertions for each skill
80
+ for skill_name, metrics in skill_metrics.items():
81
+ if metrics["total_invocations"] < MIN_INVOCATIONS:
82
+ continue
83
+
84
+ r = self._upsert_skill_assertion(conn, profile_id, skill_name, metrics)
85
+ result[f"assertions_{r}"] = result.get(f"assertions_{r}", 0) + 1
86
+
87
+ # Step 5: Detect skill correlations (pairs used together)
88
+ correlations = self._detect_skill_correlations(skill_invocations)
89
+ for pair, corr_data in correlations.items():
90
+ if corr_data["count"] >= 3:
91
+ self._upsert_correlation_assertion(
92
+ conn, profile_id, pair, corr_data,
93
+ )
94
+
95
+ conn.commit()
96
+ except Exception as exc:
97
+ logger.warning("Skill performance mining failed: %s", exc)
98
+ result["error"] = str(exc)
99
+ finally:
100
+ conn.close()
101
+
102
+ logger.info(
103
+ "Skill performance mining: %d skills, %d assertions",
104
+ result["skills_found"],
105
+ result.get("assertions_created", 0) + result.get("assertions_reinforced", 0),
106
+ )
107
+ return result
108
+
109
+ def _get_skill_events(
110
+ self, conn: sqlite3.Connection, profile_id: str,
111
+ ) -> list[dict]:
112
+ """Get all Skill tool events with enriched data."""
113
+ rows = conn.execute(
114
+ "SELECT id, session_id, tool_name, event_type, input_summary, "
115
+ "output_summary, project_path, created_at "
116
+ "FROM tool_events "
117
+ "WHERE profile_id = ? AND tool_name = 'Skill' "
118
+ "ORDER BY created_at ASC",
119
+ (profile_id,),
120
+ ).fetchall()
121
+ return [dict(r) for r in rows]
122
+
123
+ def _parse_skill_invocations(self, skill_events: list[dict]) -> list[dict]:
124
+ """Extract skill name and args from input_summary JSON."""
125
+ invocations = []
126
+
127
+ for event in skill_events:
128
+ input_raw = event.get("input_summary", "")
129
+ output_raw = event.get("output_summary", "")
130
+ skill_name = ""
131
+
132
+ # Try extracting from input_summary (enriched hook format)
133
+ if input_raw:
134
+ try:
135
+ inp = json.loads(input_raw) if input_raw.startswith("{") else {}
136
+ skill_name = inp.get("skill", "")
137
+ except (json.JSONDecodeError, TypeError):
138
+ pass
139
+
140
+ # Fallback: try output_summary (ECC ingestion format)
141
+ if not skill_name and output_raw:
142
+ try:
143
+ out = json.loads(output_raw) if output_raw.startswith("{") else {}
144
+ skill_name = out.get("commandName", "")
145
+ except (json.JSONDecodeError, TypeError):
146
+ pass
147
+
148
+ if not skill_name:
149
+ continue
150
+
151
+ invocations.append({
152
+ "skill_name": skill_name,
153
+ "session_id": event.get("session_id", ""),
154
+ "event_id": event.get("id", 0),
155
+ "created_at": event.get("created_at", ""),
156
+ "project_path": event.get("project_path", ""),
157
+ })
158
+
159
+ return invocations
160
+
161
+ def _compute_skill_metrics(
162
+ self,
163
+ conn: sqlite3.Connection,
164
+ profile_id: str,
165
+ invocations: list[dict],
166
+ ) -> dict[str, dict]:
167
+ """Compute per-skill metrics using execution trace heuristic.
168
+
169
+ Outcome heuristic (conservative, labeled as APPROXIMATE):
170
+ - Signal 1 (POSITIVE): Productive tools follow (Edit, Write, Bash success)
171
+ - Signal 2 (NEGATIVE): Same Skill re-invoked within 5 min
172
+ - Signal 3 (NEGATIVE): Bash errors in next 3 events
173
+ - Signal 4 (WEAK POSITIVE): Session continues 10+ events
174
+
175
+ H-N1QUERY: Batch-loads all trace events in one query instead of N+1.
176
+ """
177
+ metrics: dict[str, dict] = defaultdict(lambda: {
178
+ "total_invocations": 0,
179
+ "positive_signals": 0,
180
+ "negative_signals": 0,
181
+ "sessions": set(),
182
+ "projects": set(),
183
+ })
184
+
185
+ if not invocations:
186
+ return {}
187
+
188
+ # H-N1QUERY: Batch-load all potential trace events in one query.
189
+ # Find the min event_id across all invocations so we can fetch
190
+ # all subsequent events in a single SELECT.
191
+ min_event_id = min(inv["event_id"] for inv in invocations)
192
+ all_trace_rows = conn.execute(
193
+ "SELECT id, tool_name, event_type, output_summary, created_at "
194
+ "FROM tool_events "
195
+ "WHERE profile_id = ? AND id > ? "
196
+ "ORDER BY id ASC",
197
+ (profile_id, min_event_id),
198
+ ).fetchall()
199
+ all_trace = [dict(r) for r in all_trace_rows]
200
+
201
+ # Build an index: for each event_id, find its position in all_trace
202
+ # so we can slice TRACE_WINDOW events after it in O(1).
203
+ trace_id_to_idx: dict[int, int] = {}
204
+ for idx, t in enumerate(all_trace):
205
+ if t["id"] not in trace_id_to_idx:
206
+ trace_id_to_idx[t["id"]] = idx
207
+
208
+ for inv in invocations:
209
+ skill = inv["skill_name"]
210
+ m = metrics[skill]
211
+ m["total_invocations"] += 1
212
+ m["sessions"].add(inv["session_id"])
213
+ if inv["project_path"]:
214
+ m["projects"].add(inv["project_path"])
215
+
216
+ # Find trace window for this invocation from pre-loaded data
217
+ # Events with id > inv["event_id"], take first TRACE_WINDOW
218
+ start_idx = 0
219
+ eid = inv["event_id"]
220
+ # The first entry in all_trace with id > eid
221
+ # Since all_trace starts at min_event_id+1 and is sorted, we
222
+ # can bisect or scan. Use the index if the next id is present.
223
+ # Simple approach: events after eid start at the position of eid+1
224
+ # or the first id > eid in the sorted list.
225
+ for candidate_id in range(eid + 1, eid + TRACE_WINDOW + 2):
226
+ if candidate_id in trace_id_to_idx:
227
+ start_idx = trace_id_to_idx[candidate_id]
228
+ break
229
+ else:
230
+ # No trace events found after this invocation
231
+ start_idx = len(all_trace)
232
+
233
+ trace_list = all_trace[start_idx:start_idx + TRACE_WINDOW]
234
+ outcome = self._evaluate_trace(skill, inv, trace_list, invocations)
235
+
236
+ if outcome > 0:
237
+ m["positive_signals"] += 1
238
+ elif outcome < 0:
239
+ m["negative_signals"] += 1
240
+
241
+ # Compute final metrics per skill
242
+ result = {}
243
+ for skill, m in metrics.items():
244
+ total = m["total_invocations"]
245
+ positive = m["positive_signals"]
246
+ negative = m["negative_signals"]
247
+
248
+ effective_score = (positive - negative) / total if total > 0 else 0.0
249
+ result[skill] = {
250
+ "total_invocations": total,
251
+ "positive_signals": positive,
252
+ "negative_signals": negative,
253
+ "effective_score": round(max(-1.0, min(1.0, effective_score)), 3),
254
+ "session_count": len(m["sessions"]),
255
+ "project_count": len(m["projects"]),
256
+ }
257
+
258
+ return result
259
+
260
+ def _evaluate_trace(
261
+ self,
262
+ skill_name: str,
263
+ invocation: dict,
264
+ trace: list[dict],
265
+ all_invocations: list[dict],
266
+ ) -> int:
267
+ """Evaluate execution trace after a Skill call. Returns +1, 0, or -1."""
268
+ if not trace:
269
+ return 0
270
+
271
+ score = 0
272
+
273
+ # Signal 1: Productive tools in trace → +1
274
+ productive_tools = {"Edit", "Write"}
275
+ if any(t["tool_name"] in productive_tools for t in trace[:TRACE_WINDOW]):
276
+ score += 1
277
+
278
+ # Signal 2: Same Skill re-invoked within RETRY_WINDOW → -1
279
+ inv_time = invocation.get("created_at", "")
280
+ for other in all_invocations:
281
+ if other["event_id"] == invocation["event_id"]:
282
+ continue
283
+ if other["skill_name"] != skill_name:
284
+ continue
285
+ if other["session_id"] != invocation["session_id"]:
286
+ continue
287
+ try:
288
+ t1 = datetime.fromisoformat(inv_time.replace("Z", "+00:00"))
289
+ t2 = datetime.fromisoformat(
290
+ other["created_at"].replace("Z", "+00:00"),
291
+ )
292
+ delta = abs((t2 - t1).total_seconds())
293
+ if 0 < delta <= RETRY_WINDOW_SECONDS:
294
+ score -= 1
295
+ break
296
+ except (ValueError, TypeError):
297
+ pass
298
+
299
+ # Signal 3: Bash errors in first 3 events → -1
300
+ for t in trace[:3]:
301
+ if t["tool_name"] == "Bash":
302
+ output = t.get("output_summary", "")
303
+ if output and any(
304
+ kw in output.lower()
305
+ for kw in ("error", "failed", "command not found", "permission denied")
306
+ ):
307
+ score -= 1
308
+ break
309
+
310
+ # Clamp to [-1, +1]
311
+ return max(-1, min(1, score))
312
+
313
+ def _detect_skill_correlations(
314
+ self, invocations: list[dict],
315
+ ) -> dict[tuple[str, str], dict]:
316
+ """Find skills frequently used together in the same session."""
317
+ session_skills: dict[str, set[str]] = defaultdict(set)
318
+ for inv in invocations:
319
+ session_skills[inv["session_id"]].add(inv["skill_name"])
320
+
321
+ pair_counts: Counter = Counter()
322
+ for skills in session_skills.values():
323
+ skill_list = sorted(skills)
324
+ for i in range(len(skill_list)):
325
+ for j in range(i + 1, len(skill_list)):
326
+ pair_counts[(skill_list[i], skill_list[j])] += 1
327
+
328
+ return {
329
+ pair: {"count": count, "sessions": count}
330
+ for pair, count in pair_counts.most_common(10)
331
+ if count >= 2
332
+ }
333
+
334
+ def _upsert_skill_assertion(
335
+ self,
336
+ conn: sqlite3.Connection,
337
+ profile_id: str,
338
+ skill_name: str,
339
+ metrics: dict,
340
+ ) -> str:
341
+ """Create or reinforce a skill performance assertion."""
342
+ now = datetime.now(timezone.utc).isoformat()
343
+ eff = metrics["effective_score"]
344
+ total = metrics["total_invocations"]
345
+
346
+ trigger = f"when considering skill {skill_name}"
347
+ action = (
348
+ f"effective score: {eff:.0%} (approximate, {total} invocations, "
349
+ f"{metrics['session_count']} sessions)"
350
+ )
351
+
352
+ assertion_id = hashlib.sha256(
353
+ f"{profile_id}:skill_perf:{skill_name}".encode(),
354
+ ).hexdigest()[:32]
355
+
356
+ existing = conn.execute(
357
+ "SELECT id, confidence FROM behavioral_assertions WHERE id = ?",
358
+ (assertion_id,),
359
+ ).fetchone()
360
+
361
+ confidence = min(0.85, max(0.3, abs(eff) * 0.8 + total / 100))
362
+
363
+ if existing:
364
+ old_conf = dict(existing)["confidence"]
365
+ new_conf = old_conf + (1.0 - old_conf) * REINFORCEMENT_NUDGE
366
+ conn.execute(
367
+ "UPDATE behavioral_assertions SET "
368
+ "action = ?, confidence = ?, evidence_count = ?, "
369
+ "reinforcement_count = reinforcement_count + 1, "
370
+ "last_reinforced_at = ?, updated_at = ? WHERE id = ?",
371
+ (action, round(min(0.95, new_conf), 4), total, now, now, assertion_id),
372
+ )
373
+ return "reinforced"
374
+ else:
375
+ conn.execute(
376
+ "INSERT INTO behavioral_assertions "
377
+ "(id, profile_id, project_path, trigger_condition, action, "
378
+ " category, confidence, evidence_count, source, created_at, updated_at) "
379
+ "VALUES (?, ?, '', ?, ?, 'skill_performance', ?, ?, 'skill_miner', ?, ?)",
380
+ (assertion_id, profile_id, trigger, action,
381
+ round(confidence, 4), total, now, now),
382
+ )
383
+ return "created"
384
+
385
+ def _upsert_correlation_assertion(
386
+ self,
387
+ conn: sqlite3.Connection,
388
+ profile_id: str,
389
+ pair: tuple[str, str],
390
+ corr_data: dict,
391
+ ) -> None:
392
+ """Create assertion for skill correlation."""
393
+ now = datetime.now(timezone.utc).isoformat()
394
+ trigger = f"when using {pair[0]}"
395
+ action = f"often paired with {pair[1]} ({corr_data['count']} sessions together)"
396
+
397
+ assertion_id = hashlib.sha256(
398
+ f"{profile_id}:skill_corr:{pair[0]}:{pair[1]}".encode(),
399
+ ).hexdigest()[:32]
400
+
401
+ existing = conn.execute(
402
+ "SELECT id FROM behavioral_assertions WHERE id = ?",
403
+ (assertion_id,),
404
+ ).fetchone()
405
+
406
+ if existing:
407
+ conn.execute(
408
+ "UPDATE behavioral_assertions SET "
409
+ "action = ?, reinforcement_count = reinforcement_count + 1, "
410
+ "last_reinforced_at = ?, updated_at = ? WHERE id = ?",
411
+ (action, now, now, assertion_id),
412
+ )
413
+ else:
414
+ conn.execute(
415
+ "INSERT INTO behavioral_assertions "
416
+ "(id, profile_id, project_path, trigger_condition, action, "
417
+ " category, confidence, evidence_count, source, created_at, updated_at) "
418
+ "VALUES (?, ?, '', ?, ?, 'skill_correlation', ?, ?, 'skill_miner', ?, ?)",
419
+ (assertion_id, profile_id, trigger, action,
420
+ round(min(0.7, corr_data["count"] / 10), 4),
421
+ corr_data["count"], now, now),
422
+ )
@@ -79,6 +79,8 @@ _ESSENTIAL_TOOLS: set[str] = {
79
79
  # v3.4.7: Two-way learning (4)
80
80
  "log_tool_event", "get_assertions",
81
81
  "reinforce_assertion", "contradict_assertion",
82
+ # v3.4.11: Skill evolution (3)
83
+ "evolve_skill", "skill_health", "skill_lineage",
82
84
  }
83
85
 
84
86
  # v3.4.4: Mesh tools — enabled if mesh_enabled in config or SLM_MCP_MESH_TOOLS=1
@@ -138,6 +140,7 @@ from superlocalmemory.mcp.resources import register_resources
138
140
  from superlocalmemory.mcp.tools_code_graph import register_code_graph_tools
139
141
  from superlocalmemory.mcp.tools_mesh import register_mesh_tools
140
142
  from superlocalmemory.mcp.tools_learning import register_learning_tools
143
+ from superlocalmemory.mcp.tools_evolution import register_evolution_tools
141
144
 
142
145
  register_core_tools(_target, get_engine)
143
146
  register_v28_tools(_target, get_engine)
@@ -148,6 +151,7 @@ register_resources(server, get_engine) # Resources always registered (not tools
148
151
  register_code_graph_tools(_target, get_engine) # CodeGraph: filtered like other tools (SLM_MCP_ALL_TOOLS=1 to show all)
149
152
  register_mesh_tools(_target, get_engine) # v3.4.4: Mesh P2P tools — ships with SLM, no separate slm-mesh needed
150
153
  register_learning_tools(_target, get_engine) # v3.4.7: Two-way learning tools
154
+ register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
151
155
 
152
156
 
153
157
  # V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.