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
@@ -265,7 +265,7 @@ def run_wizard(auto: bool = False) -> None:
265
265
  print()
266
266
 
267
267
  # -- Step 1: System check --
268
- print("─── Step 1/6: System Check ───")
268
+ print("─── Step 1/10: System Check ───")
269
269
  print()
270
270
  py_ver = platform.python_version()
271
271
  py_ok = sys.version_info >= (3, 11)
@@ -293,7 +293,7 @@ def run_wizard(auto: bool = False) -> None:
293
293
 
294
294
  # -- Step 2: Mode selection --
295
295
  print()
296
- print("─── Step 2/6: Choose Operating Mode ───")
296
+ print("─── Step 2/10: Choose Operating Mode ───")
297
297
  print()
298
298
  print(" [A] Local Guardian (recommended)")
299
299
  print(" Zero cloud. Zero LLM. Full privacy.")
@@ -342,7 +342,7 @@ def run_wizard(auto: bool = False) -> None:
342
342
 
343
343
  # -- Step 3: Code Knowledge Graph --
344
344
  print()
345
- print("─── Step 3/6: Code Knowledge Graph ───")
345
+ print("─── Step 3/10: Code Knowledge Graph ───")
346
346
  print()
347
347
  print(" CodeGraph builds a structural map of your codebase using Tree-sitter.")
348
348
  print(" It gives your AI assistant blast-radius analysis, call graphs,")
@@ -375,7 +375,7 @@ def run_wizard(auto: bool = False) -> None:
375
375
 
376
376
  # -- Step 4: Download models --
377
377
  print()
378
- print("─── Step 4/9: Download Embedding Model ───")
378
+ print("─── Step 4/10: Download Embedding Model ───")
379
379
 
380
380
  if not st_ok:
381
381
  print(" ⚠ Skipped (sentence-transformers not installed)")
@@ -386,7 +386,7 @@ def run_wizard(auto: bool = False) -> None:
386
386
  print(" ⚠ Model will download on first use (may take a few minutes)")
387
387
 
388
388
  print()
389
- print("─── Step 4b/9: Download Reranker Model ───")
389
+ print("─── Step 4b/10: Download Reranker Model ───")
390
390
 
391
391
  if not st_ok:
392
392
  print(" ⚠ Skipped (sentence-transformers not installed)")
@@ -395,7 +395,7 @@ def run_wizard(auto: bool = False) -> None:
395
395
 
396
396
  # -- Step 5: Daemon Configuration (v3.4.3) --
397
397
  print()
398
- print("─── Step 5/9: Daemon Configuration ───")
398
+ print("─── Step 5/10: Daemon Configuration ───")
399
399
  print()
400
400
  print(" The SLM daemon runs in the background for instant memory access.")
401
401
  print()
@@ -425,7 +425,7 @@ def run_wizard(auto: bool = False) -> None:
425
425
 
426
426
  # -- Step 6: Mesh Communication (v3.4.3) --
427
427
  print()
428
- print("─── Step 6/9: Mesh Communication ───")
428
+ print("─── Step 6/10: Mesh Communication ───")
429
429
  print()
430
430
  print(" SLM Mesh enables agent-to-agent P2P communication.")
431
431
  print(" Multiple AI sessions can share knowledge in real-time.")
@@ -446,7 +446,7 @@ def run_wizard(auto: bool = False) -> None:
446
446
 
447
447
  # -- Step 7: Ingestion Adapters (v3.4.3) --
448
448
  print()
449
- print("─── Step 7/9: Ingestion Adapters ───")
449
+ print("─── Step 7/10: Ingestion Adapters ───")
450
450
  print()
451
451
  print(" These let SLM learn from your email, calendar, and meetings.")
452
452
  print(" All adapters are OFF by default. You can enable them later.")
@@ -486,7 +486,7 @@ def run_wizard(auto: bool = False) -> None:
486
486
 
487
487
  # -- Step 8: Entity Compilation (v3.4.3) --
488
488
  print()
489
- print("─── Step 8/9: Entity Compilation ───")
489
+ print("─── Step 8/10: Entity Compilation ───")
490
490
  print()
491
491
  print(" Entity compilation builds knowledge summaries per person,")
492
492
  print(" project, and concept. Runs automatically during consolidation.")
@@ -505,9 +505,50 @@ def run_wizard(auto: bool = False) -> None:
505
505
  config.save()
506
506
  print(f"\n ✓ Entity compilation {'enabled' if config.entity_compilation_enabled else 'disabled'}")
507
507
 
508
- # -- Step 9: Verification --
508
+ # -- Step 9: Skill Evolution (v3.4.11) --
509
509
  print()
510
- print("─── Step 9/9: Verification ───")
510
+ print("─── Step 9/10: Skill Evolution ───")
511
+ print()
512
+ print(" SLM can automatically evolve skills that underperform.")
513
+ print(" It detects degradation, generates improvements, and verifies them blindly.")
514
+ print(" Requires an LLM backend (Claude CLI, Ollama, or API key).")
515
+ print()
516
+ print(" [Y] Enable Skill Evolution")
517
+ print(" [N] Disable (can enable later: slm config set evolution.enabled true)")
518
+ print()
519
+
520
+ if interactive:
521
+ evo_choice = _prompt(" Enable Skill Evolution? [Y/n] (default: Y): ", "y").lower()
522
+ else:
523
+ evo_choice = "y"
524
+ print(" Auto-enabling Skill Evolution (non-interactive)")
525
+
526
+ evolution_enabled = evo_choice in ("", "y", "yes")
527
+
528
+ # Write evolution config to config.json directly
529
+ # (SLMConfig.save() doesn't serialize evolution)
530
+ _SLM_HOME.mkdir(parents=True, exist_ok=True)
531
+ evo_config_path = _SLM_HOME / "config.json"
532
+ evo_cfg: dict = {}
533
+ if evo_config_path.exists():
534
+ try:
535
+ evo_cfg = json.loads(evo_config_path.read_text())
536
+ except (json.JSONDecodeError, OSError):
537
+ pass
538
+ evo_cfg["evolution"] = {
539
+ "enabled": evolution_enabled,
540
+ "backend": "auto",
541
+ }
542
+ evo_config_path.write_text(json.dumps(evo_cfg, indent=2) + "\n")
543
+
544
+ if evolution_enabled:
545
+ print(f"\n ✓ Skill Evolution enabled (backend: auto-detect)")
546
+ else:
547
+ print(f"\n ✓ Skill Evolution disabled")
548
+
549
+ # -- Step 10: Verification --
550
+ print()
551
+ print("─── Step 10/10: Verification ───")
511
552
 
512
553
  if st_ok:
513
554
  verified = _verify_installation()
@@ -537,6 +578,8 @@ def run_wizard(auto: bool = False) -> None:
537
578
  print(", Entity Compilation", end="")
538
579
  if code_graph_enabled:
539
580
  print(", CodeGraph", end="")
581
+ if evolution_enabled:
582
+ print(", Skill Evolution", end="")
540
583
  print()
541
584
  if enabled_adapters:
542
585
  print(f" Adapters: {', '.join(enabled_adapters)}")
@@ -490,6 +490,25 @@ class TemporalValidatorConfig:
490
490
  include_expired_in_history: bool = True
491
491
 
492
492
 
493
+ @dataclass(frozen=True)
494
+ class EvolutionConfig:
495
+ """Configuration for Skill Evolution Engine (v3.4.10).
496
+
497
+ OFF by default — opt in via `slm setup` (interactive) or
498
+ `slm config set evolution.enabled true` (CLI).
499
+
500
+ Backend auto-detection priority:
501
+ 1. `claude` CLI available → spawn `claude --model haiku` (ECC pattern, free)
502
+ 2. Ollama running → use Ollama (free, local)
503
+ 3. API key set → use Anthropic/OpenAI API (paid)
504
+ 4. Nothing → dashboard-only (show candidates, manual evolution)
505
+ """
506
+
507
+ enabled: bool = False # OFF by default, opt-in
508
+ backend: str = "auto" # auto, claude, ollama, anthropic, openai
509
+ max_evolutions_per_cycle: int = 3 # Budget cap per consolidation
510
+
511
+
493
512
  @dataclass(frozen=True)
494
513
  class AutoInvokeConfig:
495
514
  """Configuration for the Auto-Invoke Engine (Phase 2).
@@ -580,6 +599,7 @@ class SLMConfig:
580
599
  parameterization: ParameterizationConfig = field(
581
600
  default_factory=ParameterizationConfig,
582
601
  )
602
+ evolution: EvolutionConfig = field(default_factory=EvolutionConfig)
583
603
 
584
604
  # v3.4.3: Daemon configuration
585
605
  daemon_idle_timeout: int = 0 # 0 = 24/7 (no auto-kill). >0 = seconds before auto-kill.
@@ -657,6 +677,14 @@ class SLMConfig:
657
677
  )
658
678
  config.mesh_enabled = data.get("mesh_enabled", True)
659
679
 
680
+ # V3.4.10: Evolution config
681
+ evo = data.get("evolution", {})
682
+ if evo:
683
+ config.evolution = EvolutionConfig(**{
684
+ k: v for k, v in evo.items()
685
+ if k in EvolutionConfig.__dataclass_fields__
686
+ })
687
+
660
688
  return config
661
689
 
662
690
  def save(self, config_path: Path | None = None) -> None:
@@ -696,6 +724,13 @@ class SLMConfig:
696
724
  },
697
725
  }
698
726
 
727
+ # V3.4.11: Persist evolution config (C-CONFIGSAVE fix)
728
+ data["evolution"] = {
729
+ "enabled": self.evolution.enabled,
730
+ "backend": self.evolution.backend,
731
+ "max_evolutions_per_cycle": self.evolution.max_evolutions_per_cycle,
732
+ }
733
+
699
734
  # Preserve existing V3.3 config sections that aren't in for_mode()
700
735
  for key in ("forgetting", "quantization", "sagq", "embedding_signature", "auto_invoke"):
701
736
  if key in existing:
@@ -178,6 +178,37 @@ class ConsolidationEngine:
178
178
  logger.debug("Soft prompt generation (non-fatal): %s", exc)
179
179
  results["soft_prompts"] = {"error": str(exc)}
180
180
 
181
+ # Step 10 (v3.4.10): Mine skill performance from tool events.
182
+ # Creates per-skill assertions and skill correlation patterns.
183
+ try:
184
+ from superlocalmemory.learning.skill_performance_miner import SkillPerformanceMiner
185
+ spm = SkillPerformanceMiner(self._db.db_path)
186
+ results["skill_performance"] = spm.mine(profile_id)
187
+ except Exception as exc:
188
+ logger.debug("Skill performance mining (non-fatal): %s", exc)
189
+ results["skill_performance"] = {"error": str(exc)}
190
+
191
+ # Step 11 (v3.4.10): Skill evolution — 3-trigger system.
192
+ # Runs degradation + health check triggers. Post-session
193
+ # trigger runs separately from the Stop hook.
194
+ # Never on recall/remember hot path. Budget: max 3 per cycle.
195
+ try:
196
+ from superlocalmemory.evolution.skill_evolver import SkillEvolver
197
+ evolver = SkillEvolver(self._db.db_path)
198
+ results["skill_evolution"] = evolver.run_consolidation_cycle(profile_id)
199
+ except Exception as exc:
200
+ logger.debug("Skill evolution (non-fatal): %s", exc)
201
+ results["skill_evolution"] = {"error": str(exc)}
202
+
203
+ # Step 12 (v3.4.11): Generate soft prompts for evolved skills.
204
+ # Queries promoted evolutions and creates/updates custom soft
205
+ # prompts so the AI prefers evolved skill variants.
206
+ try:
207
+ results["evolution_soft_prompts"] = self._step12_evolution_soft_prompts(profile_id)
208
+ except Exception as exc:
209
+ logger.debug("Evolution soft prompts (non-fatal): %s", exc)
210
+ results["evolution_soft_prompts"] = {"error": str(exc)}
211
+
181
212
  results["success"] = True
182
213
  except Exception as exc:
183
214
  logger.warning(
@@ -588,6 +619,113 @@ class ConsolidationEngine:
588
619
  logger.warning("CCQ step failed (non-fatal): %s", exc)
589
620
  return {"error": str(exc)}
590
621
 
622
+ # ------------------------------------------------------------------
623
+ # Step 12: Evolution Soft Prompts
624
+ # ------------------------------------------------------------------
625
+
626
+ def _step12_evolution_soft_prompts(self, profile_id: str) -> dict[str, Any]:
627
+ """Generate soft prompts for promoted evolved skills.
628
+
629
+ Queries skill_evolution_log for promoted evolutions and creates
630
+ or updates soft_prompt_templates with category='custom' so the
631
+ AI agent prefers evolved skill variants over originals.
632
+
633
+ Uses 'custom' category because the soft_prompt_templates CHECK
634
+ constraint does not include 'skill_evolution'. The content is
635
+ prefixed with [SKILL_EVOLUTION] for easy filtering.
636
+ """
637
+ import sqlite3 as _sqlite3
638
+
639
+ db_path = str(self._db.db_path)
640
+
641
+ conn = _sqlite3.connect(db_path, timeout=10)
642
+ conn.row_factory = _sqlite3.Row
643
+
644
+ # Fetch promoted evolutions
645
+ try:
646
+ promoted_rows = conn.execute(
647
+ "SELECT id, skill_name, parent_skill_id, evolution_type, "
648
+ "mutation_summary, created_at "
649
+ "FROM skill_evolution_log "
650
+ "WHERE status = 'promoted' "
651
+ "ORDER BY created_at DESC LIMIT 20",
652
+ ).fetchall()
653
+ except _sqlite3.OperationalError:
654
+ # Table may not exist yet
655
+ conn.close()
656
+ return {"created": 0, "message": "skill_evolution_log table not found"}
657
+
658
+ created_count = 0
659
+ now = datetime.now(timezone.utc).isoformat()
660
+
661
+ for row in promoted_rows:
662
+ r = dict(row)
663
+ skill_name = r["skill_name"]
664
+ parent = r.get("parent_skill_id") or skill_name
665
+ evo_type = r["evolution_type"]
666
+ summary = r.get("mutation_summary", "")
667
+ evo_id = r["id"]
668
+
669
+ # Build prompt content
670
+ content = (
671
+ f"[SKILL_EVOLUTION] Evolved skill: '{skill_name}' "
672
+ f"({'replaces' if evo_type == 'fix' else 'extends'} '{parent}' "
673
+ f"via {evo_type}). {summary}. "
674
+ f"Use the evolved version for better results."
675
+ )
676
+
677
+ # Use a deterministic prompt_id based on the evolution record
678
+ prompt_id = f"evo-{evo_id}"
679
+
680
+ # Check if prompt already exists
681
+ existing = conn.execute(
682
+ "SELECT prompt_id FROM soft_prompt_templates WHERE prompt_id = ?",
683
+ (prompt_id,),
684
+ ).fetchone()
685
+
686
+ if existing:
687
+ # M-REPLACE: Update existing record instead of INSERT OR REPLACE
688
+ # to avoid silently dropping columns with defaults
689
+ try:
690
+ conn.execute(
691
+ "UPDATE soft_prompt_templates "
692
+ "SET content = ?, updated_at = ? "
693
+ "WHERE prompt_id = ?",
694
+ (content, now, prompt_id),
695
+ )
696
+ except _sqlite3.OperationalError as upd_exc:
697
+ logger.debug("Failed to update soft prompt %s: %s", prompt_id, upd_exc)
698
+ continue
699
+
700
+ try:
701
+ conn.execute(
702
+ "INSERT OR IGNORE INTO soft_prompt_templates "
703
+ "(prompt_id, profile_id, category, content, source_pattern_ids, "
704
+ " confidence, effectiveness, token_count, retention_score, "
705
+ " active, version, created_at, updated_at) "
706
+ "VALUES (?, ?, 'custom', ?, ?, 0.8, 0.5, ?, 1.0, 1, 1, ?, ?)",
707
+ (prompt_id, profile_id, content,
708
+ json.dumps([evo_id]),
709
+ len(content.split()), # Rough token estimate
710
+ now, now),
711
+ )
712
+ if conn.total_changes:
713
+ created_count += 1
714
+ except _sqlite3.IntegrityError:
715
+ # Unique constraint on (profile_id, category) WHERE active=1
716
+ logger.debug(
717
+ "Skipping soft prompt for %s: unique constraint on active custom",
718
+ skill_name,
719
+ )
720
+
721
+ conn.commit()
722
+ conn.close()
723
+
724
+ return {
725
+ "promoted_skills_found": len(promoted_rows),
726
+ "soft_prompts_created": created_count,
727
+ }
728
+
591
729
  # ------------------------------------------------------------------
592
730
  # Core Memory Block Storage
593
731
  # ------------------------------------------------------------------
@@ -170,7 +170,7 @@ def _worker_main() -> None:
170
170
  # a fresh worker on next request (existing mechanism in embeddings.py).
171
171
  # V3.3.21: Configurable via SLM_EMBED_WORKER_RSS_LIMIT_MB (default 2500MB).
172
172
  import resource
173
- _rss_limit = int(os.environ.get("SLM_EMBED_WORKER_RSS_LIMIT_MB", 2500))
173
+ _rss_limit = int(os.environ.get("SLM_EMBED_WORKER_RSS_LIMIT_MB", 4000))
174
174
  rss_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 / 1024
175
175
  if rss_mb > _rss_limit:
176
176
  sys.exit(0)
@@ -139,6 +139,20 @@ class MemoryEngine:
139
139
  except Exception as exc:
140
140
  logger.debug("V3.4.7 schema migration: %s", exc)
141
141
 
142
+ # V3.4.10: Apply "Fortress" schema (backup_destinations, entity_blacklist)
143
+ try:
144
+ from superlocalmemory.storage.schema_v3410 import apply_v3410_schema
145
+ apply_v3410_schema(str(self._db.db_path))
146
+ except Exception as exc:
147
+ logger.debug("V3.4.10 schema migration: %s", exc)
148
+
149
+ # V3.4.11: Apply "Scale-Ready" schema (pinned_facts, backend_status, fact_consolidations)
150
+ try:
151
+ from superlocalmemory.storage.schema_v3411 import apply_v3411_schema
152
+ apply_v3411_schema(str(self._db.db_path))
153
+ except Exception as exc:
154
+ logger.debug("V3.4.11 schema migration: %s", exc)
155
+
142
156
  self._embedder = init_embedder(self._config)
143
157
 
144
158
  if self._caps.llm_fact_extraction:
@@ -377,6 +391,11 @@ class MemoryEngine:
377
391
  def close(self) -> None:
378
392
  if self._maintenance_scheduler is not None:
379
393
  self._maintenance_scheduler.stop()
394
+ if self._db is not None:
395
+ try:
396
+ self._db.close()
397
+ except Exception:
398
+ pass
380
399
  self._initialized = False
381
400
 
382
401
  @property