superlocalmemory 3.4.10 → 3.4.12

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 (47) hide show
  1. package/README.md +17 -11
  2. package/docs/skill-evolution.md +77 -10
  3. package/ide/hooks/tool-event-hook.sh +4 -4
  4. package/package.json +1 -1
  5. package/pyproject.toml +3 -2
  6. package/src/superlocalmemory/cli/commands.py +170 -0
  7. package/src/superlocalmemory/cli/main.py +21 -0
  8. package/src/superlocalmemory/cli/setup_wizard.py +54 -11
  9. package/src/superlocalmemory/core/config.py +35 -0
  10. package/src/superlocalmemory/core/consolidation_engine.py +128 -0
  11. package/src/superlocalmemory/core/embedding_worker.py +1 -1
  12. package/src/superlocalmemory/core/engine.py +12 -0
  13. package/src/superlocalmemory/core/fact_consolidator.py +425 -0
  14. package/src/superlocalmemory/core/graph_pruner.py +290 -0
  15. package/src/superlocalmemory/core/maintenance_scheduler.py +20 -0
  16. package/src/superlocalmemory/core/recall_pipeline.py +9 -0
  17. package/src/superlocalmemory/core/tier_manager.py +325 -0
  18. package/src/superlocalmemory/encoding/entity_resolver.py +6 -5
  19. package/src/superlocalmemory/evolution/__init__.py +29 -0
  20. package/src/superlocalmemory/evolution/blind_verifier.py +115 -0
  21. package/src/superlocalmemory/evolution/evolution_store.py +302 -0
  22. package/src/superlocalmemory/evolution/mutation_generator.py +181 -0
  23. package/src/superlocalmemory/evolution/skill_evolver.py +555 -0
  24. package/src/superlocalmemory/evolution/triggers.py +367 -0
  25. package/src/superlocalmemory/evolution/types.py +92 -0
  26. package/src/superlocalmemory/hooks/hook_handlers.py +13 -0
  27. package/src/superlocalmemory/learning/skill_performance_miner.py +44 -11
  28. package/src/superlocalmemory/mcp/server.py +4 -0
  29. package/src/superlocalmemory/mcp/tools_evolution.py +338 -0
  30. package/src/superlocalmemory/retrieval/engine.py +98 -11
  31. package/src/superlocalmemory/retrieval/entity_channel.py +118 -0
  32. package/src/superlocalmemory/retrieval/forgetting_filter.py +22 -7
  33. package/src/superlocalmemory/retrieval/strategy.py +2 -2
  34. package/src/superlocalmemory/server/routes/behavioral.py +19 -15
  35. package/src/superlocalmemory/server/routes/evolution.py +213 -0
  36. package/src/superlocalmemory/server/routes/tiers.py +195 -0
  37. package/src/superlocalmemory/server/unified_daemon.py +39 -5
  38. package/src/superlocalmemory/storage/schema_v3411.py +149 -0
  39. package/src/superlocalmemory/ui/index.html +5 -2
  40. package/src/superlocalmemory/ui/js/lifecycle.js +83 -0
  41. package/src/superlocalmemory/ui/js/ng-skills.js +394 -10
  42. package/src/superlocalmemory.egg-info/PKG-INFO +614 -0
  43. package/src/superlocalmemory.egg-info/SOURCES.txt +335 -0
  44. package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
  45. package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
  46. package/src/superlocalmemory.egg-info/requires.txt +55 -0
  47. package/src/superlocalmemory.egg-info/top_level.txt +1 -0
@@ -0,0 +1,367 @@
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
+ """Evolution Triggers — detect when skills need evolution.
6
+
7
+ 3-trigger system (adopted from OpenSpace):
8
+ 1. Post-session analysis: session ends, scan for skill failures
9
+ 2. Skill degradation: behavioral assertion confidence drops
10
+ 3. Periodic health check: consolidation cycle scans all skills
11
+
12
+ All triggers are zero-LLM, zero-embedding. Pure SQLite queries.
13
+ They produce EvolutionCandidates — the evolver decides what to do.
14
+
15
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import logging
23
+ import re
24
+ import sqlite3
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+
28
+ from superlocalmemory.evolution.types import (
29
+ EvolutionCandidate,
30
+ EvolutionType,
31
+ TriggerType,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # Thresholds — conservative, matching C1 design doc
37
+ DEGRADATION_THRESHOLD = 0.4 # Effective score below this → FIX candidate
38
+ RECOVERY_THRESHOLD = 0.6 # Above this → skill recovered, prune from addressed
39
+ DERIVED_THRESHOLD = 0.55 # Moderate effectiveness → DERIVED candidate
40
+ MIN_INVOCATIONS = 5 # Don't trigger on insufficient data
41
+ NEGATIVE_SIGNALS_THRESHOLD = 2 # Min negative signals in one session for post-session trigger
42
+ MAX_RSS_MB = 1024 # Skip evolution if process exceeds 1GB RSS
43
+
44
+
45
+ def _check_memory_pressure() -> bool:
46
+ """Return True if process RSS exceeds threshold. Skip evolution if so."""
47
+ try:
48
+ import psutil
49
+ rss_mb = psutil.Process().memory_info().rss / (1024 * 1024)
50
+ if rss_mb > MAX_RSS_MB:
51
+ logger.warning(
52
+ "Memory pressure: %dMB RSS > %dMB limit, skipping evolution",
53
+ int(rss_mb), MAX_RSS_MB,
54
+ )
55
+ return True
56
+ except ImportError:
57
+ pass
58
+ return False
59
+
60
+
61
+ class PostSessionTrigger:
62
+ """Trigger 1: Analyze a completed session for skill failures.
63
+
64
+ Scans tool_events for the given session_id. If any Skill call
65
+ had 2+ negative signals (retries, errors), creates a FIX candidate.
66
+
67
+ Zero-LLM. Runs in ~10ms on indexed table.
68
+ """
69
+
70
+ def __init__(self, db_path: str | Path):
71
+ self._db_path = str(db_path)
72
+
73
+ def scan(self, session_id: str, profile_id: str = "default") -> list[EvolutionCandidate]:
74
+ if _check_memory_pressure():
75
+ return []
76
+
77
+ candidates = []
78
+ conn = sqlite3.connect(self._db_path, timeout=10)
79
+ conn.row_factory = sqlite3.Row
80
+
81
+ try:
82
+ # Get all Skill events for this session
83
+ rows = conn.execute(
84
+ "SELECT id, tool_name, input_summary, output_summary, created_at "
85
+ "FROM tool_events "
86
+ "WHERE session_id = ? AND profile_id = ? AND tool_name = 'Skill' "
87
+ "ORDER BY id ASC",
88
+ (session_id, profile_id),
89
+ ).fetchall()
90
+
91
+ if not rows:
92
+ return []
93
+
94
+ # Parse skill names and count per-skill signals
95
+ skill_signals: dict[str, dict] = {}
96
+ for row in rows:
97
+ d = dict(row)
98
+ skill_name = self._extract_skill_name(d)
99
+ if not skill_name:
100
+ continue
101
+
102
+ if skill_name not in skill_signals:
103
+ skill_signals[skill_name] = {
104
+ "positive": 0, "negative": 0, "event_ids": [],
105
+ }
106
+ skill_signals[skill_name]["event_ids"].append(d["id"])
107
+
108
+ # Check execution traces for each skill
109
+ for skill_name, signals in skill_signals.items():
110
+ for event_id in signals["event_ids"]:
111
+ trace = conn.execute(
112
+ "SELECT tool_name, output_summary FROM tool_events "
113
+ "WHERE profile_id = ? AND id > ? ORDER BY id ASC LIMIT 5",
114
+ (profile_id, event_id),
115
+ ).fetchall()
116
+
117
+ has_error = any(
118
+ "error" in (dict(t).get("output_summary", "") or "").lower()
119
+ for t in trace[:3]
120
+ if dict(t)["tool_name"] == "Bash"
121
+ )
122
+ has_retry = any(
123
+ dict(t)["tool_name"] == "Skill"
124
+ for t in trace[:3]
125
+ )
126
+
127
+ if has_error or has_retry:
128
+ signals["negative"] += 1
129
+ else:
130
+ signals["positive"] += 1
131
+
132
+ total_invocations = len(signals["event_ids"])
133
+ if signals["negative"] >= NEGATIVE_SIGNALS_THRESHOLD and total_invocations >= MIN_INVOCATIONS:
134
+ evidence = (
135
+ f"{signals['negative']} negative signals in session {session_id}",
136
+ f"{signals['positive']} positive signals",
137
+ )
138
+ candidates.append(EvolutionCandidate(
139
+ skill_name=skill_name,
140
+ evolution_type=EvolutionType.FIX,
141
+ trigger=TriggerType.POST_SESSION,
142
+ evidence=evidence,
143
+ effective_score=signals["positive"] / max(1, signals["positive"] + signals["negative"]),
144
+ invocation_count=len(signals["event_ids"]),
145
+ session_id=session_id,
146
+ ))
147
+
148
+ except Exception as exc:
149
+ logger.debug("Post-session trigger scan failed: %s", exc)
150
+ finally:
151
+ conn.close()
152
+
153
+ return candidates
154
+
155
+ def _extract_skill_name(self, event: dict) -> str:
156
+ for field_key in ("input_summary", "output_summary"):
157
+ raw = event.get(field_key, "") or ""
158
+ if not raw:
159
+ continue
160
+ try:
161
+ parsed = json.loads(raw)
162
+ name = parsed.get("skill") or parsed.get("commandName", "")
163
+ if name:
164
+ return name
165
+ except (json.JSONDecodeError, TypeError):
166
+ # M-JSONPARSE: Fallback for non-JSON plain text
167
+ m = re.search(r"skill[:\s]+['\"]?(\S+)", raw, re.IGNORECASE)
168
+ if m:
169
+ return m.group(1).strip("'\"")
170
+ return ""
171
+
172
+
173
+ class DegradationTrigger:
174
+ """Trigger 2: Detect skills with declining performance assertions.
175
+
176
+ Scans behavioral_assertions for skill_performance category.
177
+ If a skill's confidence dropped below threshold → FIX candidate.
178
+ If moderate with specific failure pattern → DERIVED candidate.
179
+
180
+ Zero-LLM. Runs in ~5ms.
181
+ """
182
+
183
+ def __init__(self, db_path: str | Path):
184
+ self._db_path = str(db_path)
185
+
186
+ def scan(self, profile_id: str = "default") -> list[EvolutionCandidate]:
187
+ if _check_memory_pressure():
188
+ return []
189
+
190
+ candidates = []
191
+ conn = sqlite3.connect(self._db_path, timeout=10)
192
+ conn.row_factory = sqlite3.Row
193
+
194
+ try:
195
+ rows = conn.execute(
196
+ "SELECT trigger_condition, action, confidence, evidence_count "
197
+ "FROM behavioral_assertions "
198
+ "WHERE profile_id = ? AND category = 'skill_performance' "
199
+ "AND confidence > 0",
200
+ (profile_id,),
201
+ ).fetchall()
202
+
203
+ for row in rows:
204
+ d = dict(row)
205
+ # H-TRIGGERPARSE: Extract skill name robustly.
206
+ # Primary: regex on trigger_condition. Fallback: old replace approach.
207
+ skill_name = ""
208
+ tc = d["trigger_condition"]
209
+ m = re.search(r"skill\s+(\S+)", tc)
210
+ if m:
211
+ skill_name = m.group(1)
212
+ else:
213
+ skill_name = tc.replace("when considering skill ", "")
214
+ confidence = d["confidence"]
215
+ evidence = d["evidence_count"]
216
+
217
+ if evidence < MIN_INVOCATIONS:
218
+ continue
219
+
220
+ # Parse effective score from action text
221
+ effective_score = self._parse_effective_score(d["action"])
222
+
223
+ if effective_score < DEGRADATION_THRESHOLD:
224
+ candidates.append(EvolutionCandidate(
225
+ skill_name=skill_name,
226
+ evolution_type=EvolutionType.FIX,
227
+ trigger=TriggerType.DEGRADATION,
228
+ evidence=(d["action"],),
229
+ effective_score=effective_score,
230
+ invocation_count=evidence,
231
+ ))
232
+ elif effective_score < DERIVED_THRESHOLD:
233
+ candidates.append(EvolutionCandidate(
234
+ skill_name=skill_name,
235
+ evolution_type=EvolutionType.DERIVED,
236
+ trigger=TriggerType.DEGRADATION,
237
+ evidence=(d["action"],),
238
+ effective_score=effective_score,
239
+ invocation_count=evidence,
240
+ ))
241
+
242
+ except Exception as exc:
243
+ logger.debug("Degradation trigger scan failed: %s", exc)
244
+ finally:
245
+ conn.close()
246
+
247
+ return candidates
248
+
249
+ def get_active_degraded(self, profile_id: str = "default") -> set[str]:
250
+ """Return names of currently degraded skills (for anti-loop pruning)."""
251
+ conn = sqlite3.connect(self._db_path, timeout=10)
252
+ conn.row_factory = sqlite3.Row
253
+ degraded = set()
254
+ try:
255
+ rows = conn.execute(
256
+ "SELECT trigger_condition, action FROM behavioral_assertions "
257
+ "WHERE profile_id = ? AND category = 'skill_performance'",
258
+ (profile_id,),
259
+ ).fetchall()
260
+ for row in rows:
261
+ d = dict(row)
262
+ score = self._parse_effective_score(d["action"])
263
+ if score < RECOVERY_THRESHOLD:
264
+ tc = d["trigger_condition"]
265
+ m = re.search(r"skill\s+(\S+)", tc)
266
+ name = m.group(1) if m else tc.replace("when considering skill ", "")
267
+ degraded.add(name)
268
+ except Exception:
269
+ pass
270
+ finally:
271
+ conn.close()
272
+ return degraded
273
+
274
+ def _parse_effective_score(self, action_text: str) -> float:
275
+ """Extract effective score from assertion action text."""
276
+ import re
277
+ match = re.search(r"effective score:\s*([-\d.]+)%", action_text)
278
+ if match:
279
+ return float(match.group(1)) / 100.0
280
+ return 0.5
281
+
282
+
283
+ class HealthCheckTrigger:
284
+ """Trigger 3: Periodic scan of all skill entities.
285
+
286
+ Runs every N consolidation cycles. Checks Entity Explorer for
287
+ skill entities with low performance.
288
+
289
+ Cycle count is persisted to the evolution_cycle_state table so it
290
+ survives process restarts (H-CYCLECNT fix).
291
+
292
+ Zero-LLM. Runs in ~20ms.
293
+ """
294
+
295
+ _STATE_KEY = "health_check_cycle_count"
296
+
297
+ def __init__(self, db_path: str | Path):
298
+ self._db_path = str(db_path)
299
+ self._check_every_n = 3 # Every 3rd consolidation (~18h)
300
+
301
+ def _read_cycle_count(self) -> int:
302
+ """Read persisted cycle count from DB."""
303
+ conn = sqlite3.connect(self._db_path, timeout=10)
304
+ try:
305
+ row = conn.execute(
306
+ "SELECT value FROM evolution_cycle_state WHERE key = ?",
307
+ (self._STATE_KEY,),
308
+ ).fetchone()
309
+ return int(row[0]) if row else 0
310
+ except sqlite3.OperationalError:
311
+ # Table may not exist yet
312
+ return 0
313
+ finally:
314
+ conn.close()
315
+
316
+ def _write_cycle_count(self, count: int) -> None:
317
+ """Persist cycle count to DB.
318
+
319
+ Relies on evolution_cycle_state table created by EvolutionStore.
320
+ Falls back to CREATE IF NOT EXISTS for standalone use.
321
+ """
322
+ conn = sqlite3.connect(self._db_path, timeout=10)
323
+ try:
324
+ # Table schema matches evolution_store.py: value INTEGER
325
+ conn.execute(
326
+ "CREATE TABLE IF NOT EXISTS evolution_cycle_state "
327
+ "(key TEXT PRIMARY KEY, value INTEGER DEFAULT 0, updated_at TEXT)",
328
+ )
329
+ now = datetime.now(timezone.utc).isoformat()
330
+ conn.execute(
331
+ "INSERT OR REPLACE INTO evolution_cycle_state (key, value, updated_at) "
332
+ "VALUES (?, ?, ?)",
333
+ (self._STATE_KEY, count, now),
334
+ )
335
+ conn.commit()
336
+ except sqlite3.OperationalError as exc:
337
+ logger.warning("Failed to persist cycle count: %s", exc)
338
+ finally:
339
+ conn.close()
340
+
341
+ def should_run(self) -> bool:
342
+ cycle_count = self._read_cycle_count() + 1
343
+ self._write_cycle_count(cycle_count)
344
+ return cycle_count % self._check_every_n == 0
345
+
346
+ def scan(self, profile_id: str = "default") -> list[EvolutionCandidate]:
347
+ if _check_memory_pressure():
348
+ return []
349
+ if not self.should_run():
350
+ return []
351
+
352
+ # Delegate to DegradationTrigger — same logic, different trigger label
353
+ deg = DegradationTrigger(self._db_path)
354
+ deg_candidates = deg.scan(profile_id)
355
+
356
+ # Re-label as health_check trigger
357
+ return [
358
+ EvolutionCandidate(
359
+ skill_name=c.skill_name,
360
+ evolution_type=c.evolution_type,
361
+ trigger=TriggerType.HEALTH_CHECK,
362
+ evidence=c.evidence,
363
+ effective_score=c.effective_score,
364
+ invocation_count=c.invocation_count,
365
+ )
366
+ for c in deg_candidates
367
+ ]
@@ -0,0 +1,92 @@
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 Evolution type definitions.
6
+
7
+ Immutable data classes for evolution candidates, records, and lineage.
8
+ All types are frozen dataclasses — no mutation after creation.
9
+
10
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from typing import Optional
18
+
19
+
20
+ class EvolutionType(str, Enum):
21
+ """How the skill is being evolved."""
22
+ FIX = "fix" # Repair broken skill in-place
23
+ DERIVED = "derived" # Create specialized variant
24
+ CAPTURED = "captured" # Extract new skill from patterns
25
+
26
+
27
+ class TriggerType(str, Enum):
28
+ """What triggered the evolution."""
29
+ POST_SESSION = "post_session" # Session Stop hook analysis
30
+ DEGRADATION = "degradation" # Behavioral assertion confidence drop
31
+ HEALTH_CHECK = "health_check" # Periodic consolidation scan
32
+
33
+
34
+ class EvolutionStatus(str, Enum):
35
+ """Pipeline status."""
36
+ CANDIDATE = "candidate" # Detected, not yet confirmed
37
+ CONFIRMED = "confirmed" # LLM gate passed
38
+ MUTATED = "mutated" # New SKILL.md generated
39
+ VERIFIED = "verified" # Blind verification passed
40
+ PROMOTED = "promoted" # Live — evolved skill active
41
+ REJECTED = "rejected" # Failed verification or gate
42
+ FAILED = "failed" # Error during evolution
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class EvolutionCandidate:
47
+ """A skill flagged for potential evolution."""
48
+ skill_name: str
49
+ evolution_type: EvolutionType
50
+ trigger: TriggerType
51
+ evidence: tuple[str, ...] = ()
52
+ effective_score: float = 0.0
53
+ invocation_count: int = 0
54
+ session_id: str = ""
55
+ project_path: str = ""
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class EvolutionRecord:
60
+ """Persisted record of an evolution attempt."""
61
+ id: str
62
+ skill_name: str
63
+ parent_skill_id: Optional[str]
64
+ evolution_type: EvolutionType
65
+ trigger: TriggerType
66
+ generation: int = 0
67
+ status: EvolutionStatus = EvolutionStatus.CANDIDATE
68
+ mutation_summary: str = ""
69
+ evidence: tuple[str, ...] = ()
70
+ original_content: str = ""
71
+ evolved_content: str = ""
72
+ content_diff: str = ""
73
+ blind_verified: bool = False
74
+ rejection_reason: str = ""
75
+ created_at: str = ""
76
+ completed_at: str = ""
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class SkillLineage:
81
+ """Lineage metadata for an evolved skill."""
82
+ skill_id: str
83
+ parent_skill_id: Optional[str]
84
+ evolution_type: EvolutionType
85
+ generation: int
86
+ trigger: TriggerType
87
+ mutation_summary: str = ""
88
+ created_at: str = ""
89
+
90
+ @property
91
+ def is_root(self) -> bool:
92
+ return self.parent_skill_id is None
@@ -301,6 +301,19 @@ def _hook_stop() -> None:
301
301
  except Exception:
302
302
  pass
303
303
 
304
+ # --- Post-session skill evolution trigger (best-effort) ---
305
+ try:
306
+ session_id = os.environ.get("CLAUDE_SESSION_ID", "")
307
+ if session_id:
308
+ subprocess.Popen(
309
+ ["slm", "evolve", "--session", session_id],
310
+ stdout=subprocess.DEVNULL,
311
+ stderr=subprocess.DEVNULL,
312
+ start_new_session=True,
313
+ )
314
+ except Exception:
315
+ pass
316
+
304
317
  # --- Auto-consolidation (if >24h since last run) ---
305
318
  _maybe_consolidate()
306
319
 
@@ -171,6 +171,8 @@ class SkillPerformanceMiner:
171
171
  - Signal 2 (NEGATIVE): Same Skill re-invoked within 5 min
172
172
  - Signal 3 (NEGATIVE): Bash errors in next 3 events
173
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.
174
176
  """
175
177
  metrics: dict[str, dict] = defaultdict(lambda: {
176
178
  "total_invocations": 0,
@@ -180,6 +182,29 @@ class SkillPerformanceMiner:
180
182
  "projects": set(),
181
183
  })
182
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
+
183
208
  for inv in invocations:
184
209
  skill = inv["skill_name"]
185
210
  m = metrics[skill]
@@ -188,16 +213,24 @@ class SkillPerformanceMiner:
188
213
  if inv["project_path"]:
189
214
  m["projects"].add(inv["project_path"])
190
215
 
191
- # Get surrounding tool events for execution trace
192
- trace = conn.execute(
193
- "SELECT tool_name, event_type, output_summary, created_at "
194
- "FROM tool_events "
195
- "WHERE profile_id = ? AND id > ? "
196
- "ORDER BY id ASC LIMIT ?",
197
- (profile_id, inv["event_id"], TRACE_WINDOW),
198
- ).fetchall()
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)
199
232
 
200
- trace_list = [dict(r) for r in trace]
233
+ trace_list = all_trace[start_idx:start_idx + TRACE_WINDOW]
201
234
  outcome = self._evaluate_trace(skill, inv, trace_list, invocations)
202
235
 
203
236
  if outcome > 0:
@@ -318,7 +351,7 @@ class SkillPerformanceMiner:
318
351
 
319
352
  assertion_id = hashlib.sha256(
320
353
  f"{profile_id}:skill_perf:{skill_name}".encode(),
321
- ).hexdigest()[:16]
354
+ ).hexdigest()[:32]
322
355
 
323
356
  existing = conn.execute(
324
357
  "SELECT id, confidence FROM behavioral_assertions WHERE id = ?",
@@ -363,7 +396,7 @@ class SkillPerformanceMiner:
363
396
 
364
397
  assertion_id = hashlib.sha256(
365
398
  f"{profile_id}:skill_corr:{pair[0]}:{pair[1]}".encode(),
366
- ).hexdigest()[:16]
399
+ ).hexdigest()[:32]
367
400
 
368
401
  existing = conn.execute(
369
402
  "SELECT id FROM behavioral_assertions WHERE id = ?",
@@ -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.