superlocalmemory 3.7.7 → 3.8.0

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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -2
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +96 -12
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -32,6 +32,7 @@ logger = logging.getLogger(__name__)
32
32
  _SCHEMA_DDL = """
33
33
  CREATE TABLE IF NOT EXISTS skill_evolution_log (
34
34
  id TEXT PRIMARY KEY,
35
+ profile_id TEXT NOT NULL DEFAULT 'default',
35
36
  skill_name TEXT NOT NULL,
36
37
  parent_skill_id TEXT,
37
38
  evolution_type TEXT NOT NULL,
@@ -49,14 +50,16 @@ CREATE TABLE IF NOT EXISTS skill_evolution_log (
49
50
  completed_at TEXT
50
51
  );
51
52
 
52
- CREATE INDEX IF NOT EXISTS idx_evo_skill ON skill_evolution_log(skill_name);
53
- CREATE INDEX IF NOT EXISTS idx_evo_status ON skill_evolution_log(status);
54
- CREATE INDEX IF NOT EXISTS idx_evo_created ON skill_evolution_log(created_at);
53
+ CREATE INDEX IF NOT EXISTS idx_evo_skill ON skill_evolution_log(profile_id, skill_name);
54
+ CREATE INDEX IF NOT EXISTS idx_evo_status ON skill_evolution_log(profile_id, status);
55
+ CREATE INDEX IF NOT EXISTS idx_evo_created ON skill_evolution_log(profile_id, created_at);
55
56
 
56
57
  CREATE TABLE IF NOT EXISTS evolution_cycle_state (
57
- key TEXT PRIMARY KEY,
58
+ profile_id TEXT NOT NULL DEFAULT 'default',
59
+ key TEXT NOT NULL,
58
60
  value INTEGER DEFAULT 0,
59
- updated_at TEXT
61
+ updated_at TEXT,
62
+ PRIMARY KEY (profile_id, key)
60
63
  );
61
64
  """
62
65
 
@@ -77,6 +80,11 @@ class EvolutionStore:
77
80
  def _ensure_schema(self) -> None:
78
81
  conn = sqlite3.connect(self._db_path, timeout=10)
79
82
  try:
83
+ # Migrate an existing pre-isolation DB BEFORE running the schema DDL:
84
+ # the DDL creates indexes on profile_id, which would fail on a legacy
85
+ # table that still lacks the column. On a fresh DB the migration is a
86
+ # no-op (tables absent) and the DDL creates everything correctly.
87
+ self._migrate_profile_isolation(conn)
80
88
  conn.executescript(_SCHEMA_DDL)
81
89
  conn.commit()
82
90
  except sqlite3.OperationalError as exc:
@@ -84,56 +92,113 @@ class EvolutionStore:
84
92
  finally:
85
93
  conn.close()
86
94
 
87
- def reset_cycle(self) -> None:
88
- """Reset per-cycle counters. Call at start of each consolidation."""
95
+ def _migrate_profile_isolation(self, conn: sqlite3.Connection) -> None:
96
+ """Self-migrate pre-profile-isolation DBs to add per-profile scoping.
97
+
98
+ These tables are store-owned (created lazily here, not by the schema
99
+ migration runner which fires before this store exists), so the store
100
+ owns their upgrade too. Idempotent: only alters when the column/PK is
101
+ missing. Existing rows backfill to 'default' — the historical profile.
102
+ """
103
+ # skill_evolution_log: additive column (safe, preserves rows).
104
+ cols = {r[1] for r in conn.execute(
105
+ "PRAGMA table_info(skill_evolution_log)").fetchall()}
106
+ if cols and "profile_id" not in cols:
107
+ conn.execute(
108
+ "ALTER TABLE skill_evolution_log "
109
+ "ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
110
+ )
111
+ conn.execute(
112
+ "CREATE INDEX IF NOT EXISTS idx_evo_skill "
113
+ "ON skill_evolution_log(profile_id, skill_name)"
114
+ )
115
+
116
+ # evolution_cycle_state: PK changes from (key) to (profile_id, key).
117
+ # SQLite cannot alter a PK in place → rebuild + copy, backfilling
118
+ # existing counters to the 'default' profile.
119
+ cyc_cols = {r[1] for r in conn.execute(
120
+ "PRAGMA table_info(evolution_cycle_state)").fetchall()}
121
+ if cyc_cols and "profile_id" not in cyc_cols:
122
+ conn.execute(
123
+ "ALTER TABLE evolution_cycle_state RENAME TO _evo_cycle_old"
124
+ )
125
+ conn.execute(
126
+ "CREATE TABLE evolution_cycle_state ("
127
+ " profile_id TEXT NOT NULL DEFAULT 'default',"
128
+ " key TEXT NOT NULL,"
129
+ " value INTEGER DEFAULT 0,"
130
+ " updated_at TEXT,"
131
+ " PRIMARY KEY (profile_id, key))"
132
+ )
133
+ conn.execute(
134
+ "INSERT INTO evolution_cycle_state "
135
+ "(profile_id, key, value, updated_at) "
136
+ "SELECT 'default', key, value, updated_at FROM _evo_cycle_old"
137
+ )
138
+ conn.execute("DROP TABLE _evo_cycle_old")
139
+
140
+ def reset_cycle(self, profile_id: str) -> None:
141
+ """Reset per-cycle counters for one profile. Called at cycle start.
142
+
143
+ The evolve budget is per-profile: one profile exhausting its budget
144
+ must never block another profile from evolving.
145
+ """
89
146
  now = datetime.now(timezone.utc).isoformat()
90
147
  conn = sqlite3.connect(self._db_path, timeout=10)
91
148
  try:
92
149
  conn.execute(
93
- "INSERT OR REPLACE INTO evolution_cycle_state (key, value, updated_at) "
94
- "VALUES ('cycle_count', 0, ?)",
95
- (now,),
150
+ "INSERT OR REPLACE INTO evolution_cycle_state "
151
+ "(profile_id, key, value, updated_at) "
152
+ "VALUES (?, 'cycle_count', 0, ?)",
153
+ (profile_id, now),
96
154
  )
97
155
  conn.commit()
98
156
  finally:
99
157
  conn.close()
100
158
 
101
- def can_evolve(self) -> bool:
102
- """Check if budget allows another evolution this cycle."""
159
+ def can_evolve(self, profile_id: str) -> bool:
160
+ """Check if this profile's budget allows another evolution this cycle."""
103
161
  conn = sqlite3.connect(self._db_path, timeout=10)
104
162
  try:
105
163
  row = conn.execute(
106
- "SELECT value FROM evolution_cycle_state WHERE key = 'cycle_count'",
164
+ "SELECT value FROM evolution_cycle_state "
165
+ "WHERE profile_id = ? AND key = 'cycle_count'",
166
+ (profile_id,),
107
167
  ).fetchone()
108
168
  count = row[0] if row else 0
109
169
  return count < MAX_EVOLUTIONS_PER_CYCLE
110
170
  finally:
111
171
  conn.close()
112
172
 
113
- def record_evolution_attempt(self) -> None:
114
- """Increment cycle counter in DB."""
173
+ def record_evolution_attempt(self, profile_id: str) -> None:
174
+ """Increment this profile's cycle counter in DB."""
115
175
  now = datetime.now(timezone.utc).isoformat()
116
176
  conn = sqlite3.connect(self._db_path, timeout=10)
117
177
  try:
118
178
  row = conn.execute(
119
- "SELECT value FROM evolution_cycle_state WHERE key = 'cycle_count'",
179
+ "SELECT value FROM evolution_cycle_state "
180
+ "WHERE profile_id = ? AND key = 'cycle_count'",
181
+ (profile_id,),
120
182
  ).fetchone()
121
183
  current = row[0] if row else 0
122
184
  conn.execute(
123
- "INSERT OR REPLACE INTO evolution_cycle_state (key, value, updated_at) "
124
- "VALUES ('cycle_count', ?, ?)",
125
- (current + 1, now),
185
+ "INSERT OR REPLACE INTO evolution_cycle_state "
186
+ "(profile_id, key, value, updated_at) "
187
+ "VALUES (?, 'cycle_count', ?, ?)",
188
+ (profile_id, current + 1, now),
126
189
  )
127
190
  conn.commit()
128
191
  finally:
129
192
  conn.close()
130
193
 
131
- def _get_cycle_count(self) -> int:
132
- """Read current cycle count from DB."""
194
+ def _get_cycle_count(self, profile_id: str) -> int:
195
+ """Read this profile's current cycle count from DB."""
133
196
  conn = sqlite3.connect(self._db_path, timeout=10)
134
197
  try:
135
198
  row = conn.execute(
136
- "SELECT value FROM evolution_cycle_state WHERE key = 'cycle_count'",
199
+ "SELECT value FROM evolution_cycle_state "
200
+ "WHERE profile_id = ? AND key = 'cycle_count'",
201
+ (profile_id,),
137
202
  ).fetchone()
138
203
  return row[0] if row else 0
139
204
  finally:
@@ -162,18 +227,19 @@ class EvolutionStore:
162
227
  # CRUD
163
228
  # ------------------------------------------------------------------
164
229
 
165
- def save_record(self, record: EvolutionRecord) -> None:
230
+ def save_record(self, record: EvolutionRecord, profile_id: str) -> None:
166
231
  conn = sqlite3.connect(self._db_path, timeout=10)
167
232
  try:
168
233
  conn.execute(
169
234
  "INSERT OR REPLACE INTO skill_evolution_log "
170
- "(id, skill_name, parent_skill_id, evolution_type, trigger_type, "
171
- " generation, status, mutation_summary, evidence, "
235
+ "(id, profile_id, skill_name, parent_skill_id, evolution_type, "
236
+ " trigger_type, generation, status, mutation_summary, evidence, "
172
237
  " original_content, evolved_content, content_diff, "
173
238
  " blind_verified, rejection_reason, created_at, completed_at) "
174
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
239
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
175
240
  (
176
241
  record.id,
242
+ profile_id,
177
243
  record.skill_name,
178
244
  record.parent_skill_id,
179
245
  record.evolution_type.value,
@@ -195,13 +261,14 @@ class EvolutionStore:
195
261
  finally:
196
262
  conn.close()
197
263
 
198
- def get_record(self, record_id: str) -> Optional[EvolutionRecord]:
264
+ def get_record(self, record_id: str, profile_id: str) -> Optional[EvolutionRecord]:
199
265
  conn = sqlite3.connect(self._db_path, timeout=10)
200
266
  conn.row_factory = sqlite3.Row
201
267
  try:
202
268
  row = conn.execute(
203
- "SELECT * FROM skill_evolution_log WHERE id = ?",
204
- (record_id,),
269
+ "SELECT * FROM skill_evolution_log "
270
+ "WHERE id = ? AND profile_id = ?",
271
+ (record_id, profile_id),
205
272
  ).fetchone()
206
273
  if not row:
207
274
  return None
@@ -209,68 +276,78 @@ class EvolutionStore:
209
276
  finally:
210
277
  conn.close()
211
278
 
212
- def get_skill_history(self, skill_name: str, limit: int = 20) -> list[EvolutionRecord]:
279
+ def get_skill_history(
280
+ self, skill_name: str, profile_id: str, limit: int = 20,
281
+ ) -> list[EvolutionRecord]:
213
282
  conn = sqlite3.connect(self._db_path, timeout=10)
214
283
  conn.row_factory = sqlite3.Row
215
284
  try:
216
285
  rows = conn.execute(
217
286
  "SELECT * FROM skill_evolution_log "
218
- "WHERE skill_name = ? ORDER BY created_at DESC LIMIT ?",
219
- (skill_name, limit),
287
+ "WHERE skill_name = ? AND profile_id = ? "
288
+ "ORDER BY created_at DESC LIMIT ?",
289
+ (skill_name, profile_id, limit),
220
290
  ).fetchall()
221
291
  return [self._row_to_record(dict(r)) for r in rows]
222
292
  finally:
223
293
  conn.close()
224
294
 
225
- def get_recent(self, limit: int = 10) -> list[EvolutionRecord]:
295
+ def get_recent(self, profile_id: str, limit: int = 10) -> list[EvolutionRecord]:
226
296
  conn = sqlite3.connect(self._db_path, timeout=10)
227
297
  conn.row_factory = sqlite3.Row
228
298
  try:
229
299
  rows = conn.execute(
230
300
  "SELECT * FROM skill_evolution_log "
231
- "ORDER BY created_at DESC LIMIT ?",
232
- (limit,),
301
+ "WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?",
302
+ (profile_id, limit),
233
303
  ).fetchall()
234
304
  return [self._row_to_record(dict(r)) for r in rows]
235
305
  finally:
236
306
  conn.close()
237
307
 
238
- def count_attempts(self, skill_name: str) -> int:
308
+ def count_attempts(self, skill_name: str, profile_id: str) -> int:
239
309
  conn = sqlite3.connect(self._db_path, timeout=10)
240
310
  try:
241
311
  row = conn.execute(
242
312
  "SELECT COUNT(*) FROM skill_evolution_log "
243
- "WHERE skill_name = ? AND status NOT IN ('promoted')",
244
- (skill_name,),
313
+ "WHERE skill_name = ? AND profile_id = ? "
314
+ "AND status NOT IN ('promoted')",
315
+ (skill_name, profile_id),
245
316
  ).fetchone()
246
317
  return row[0] if row else 0
247
318
  finally:
248
319
  conn.close()
249
320
 
250
- def has_exceeded_attempts(self, skill_name: str) -> bool:
251
- return self.count_attempts(skill_name) >= MAX_ATTEMPTS_PER_SKILL
321
+ def has_exceeded_attempts(self, skill_name: str, profile_id: str) -> bool:
322
+ return self.count_attempts(skill_name, profile_id) >= MAX_ATTEMPTS_PER_SKILL
252
323
 
253
- def get_stats(self) -> dict:
324
+ def get_stats(self, profile_id: str) -> dict:
254
325
  conn = sqlite3.connect(self._db_path, timeout=10)
255
326
  try:
256
327
  total = conn.execute(
257
- "SELECT COUNT(*) FROM skill_evolution_log",
328
+ "SELECT COUNT(*) FROM skill_evolution_log WHERE profile_id = ?",
329
+ (profile_id,),
258
330
  ).fetchone()[0]
259
331
  by_status = {}
260
332
  for row in conn.execute(
261
- "SELECT status, COUNT(*) FROM skill_evolution_log GROUP BY status",
333
+ "SELECT status, COUNT(*) FROM skill_evolution_log "
334
+ "WHERE profile_id = ? GROUP BY status",
335
+ (profile_id,),
262
336
  ).fetchall():
263
337
  by_status[row[0]] = row[1]
264
338
  by_type = {}
265
339
  for row in conn.execute(
266
- "SELECT evolution_type, COUNT(*) FROM skill_evolution_log GROUP BY evolution_type",
340
+ "SELECT evolution_type, COUNT(*) FROM skill_evolution_log "
341
+ "WHERE profile_id = ? GROUP BY evolution_type",
342
+ (profile_id,),
267
343
  ).fetchall():
268
344
  by_type[row[0]] = row[1]
269
345
  return {
270
346
  "total": total,
271
347
  "by_status": by_status,
272
348
  "by_type": by_type,
273
- "cycle_budget_remaining": MAX_EVOLUTIONS_PER_CYCLE - self._get_cycle_count(),
349
+ "cycle_budget_remaining":
350
+ MAX_EVOLUTIONS_PER_CYCLE - self._get_cycle_count(profile_id),
274
351
  }
275
352
  finally:
276
353
  conn.close()
@@ -162,7 +162,18 @@ ALLOWED_LLM_MODELS: frozenset[str] = frozenset({
162
162
 
163
163
  FORBIDDEN_MODEL_SUBSTRINGS: tuple[str, ...] = ("opus", "gpt-4-turbo")
164
164
 
165
- MAX_TOKENS_CAP: int = 500
165
+ # Hard ceiling on ``max_tokens`` for any single evolution LLM call
166
+ # (LLD-11). This is a CEILING, not a per-call default: each caller passes
167
+ # its own budget (confirm=100, blind-verify=500, mutation=4000). The value
168
+ # MUST stay >= the largest legitimate caller request — otherwise that call
169
+ # trips the guard in ``_dispatch_llm`` and silently fails-closed. The
170
+ # mutation step (``SkillEvolver._generate_mutation``) generates a full
171
+ # SKILL.md and requests 4000; a stale 500 here made mutation return "" on
172
+ # every cycle, killing evolution invisibly. 8000 accommodates 4000 with
173
+ # retry/large-skill headroom. Do NOT lower below 4096 without first
174
+ # shrinking that caller's request. Per-call spend is still bounded by
175
+ # EvolutionBudget (10 LLM calls/cycle x 3 cycles/day).
176
+ MAX_TOKENS_CAP: int = 8000
166
177
 
167
178
 
168
179
  # ---------------------------------------------------------------------------
@@ -0,0 +1,160 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.7.9 — evolution model selection
4
+
5
+ """Per-step LLM model selection for the skill-evolution pipeline.
6
+
7
+ v3.7.9: skill evolution defaults to the *lowest-cost* capable model for
8
+ whatever backend is active, and lets users override each step
9
+ (``evolution.mutation_model`` / ``verify_model`` / ``confirm_model``).
10
+
11
+ Two invariants this module enforces:
12
+
13
+ 1. **Lowest cost by default.** Claude backend → Haiku; Ollama → a local
14
+ (free) model. No step silently defaults to a premium model.
15
+ 2. **Generator != verifier.** Blind verification must run on a *different*
16
+ model from the generator, otherwise the generator effectively grades
17
+ its own homework. When an independent verifier isn't available for
18
+ free (Claude backend, no local Ollama), we reuse the cheapest model
19
+ and set ``independent=False`` so the caller can log the reduced-safety
20
+ condition — we never escalate the verifier to a premium model just to
21
+ force independence (that would defeat the cost goal).
22
+
23
+ This module is pure: it takes ``ollama_available`` as an argument instead
24
+ of probing, so it is fully unit-testable without network access.
25
+
26
+ Author: Varun Pratap Bhardwaj / Qualixar
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ from dataclasses import dataclass
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # Allow-listed model ids (must stay a subset of
37
+ # ``llm_dispatch.ALLOWED_LLM_MODELS``).
38
+ CHEAPEST_CLAUDE = "claude-haiku-4-5"
39
+ QUALITY_CLAUDE = "claude-sonnet-4-6"
40
+ CHEAPEST_OLLAMA = "ollama:llama3"
41
+ ALT_OLLAMA = "ollama:qwen2.5"
42
+
43
+ # Short-name → allow-listed model id. Single source of truth for aliasing;
44
+ # ``skill_evolver`` re-exports these for backward compatibility.
45
+ _MODEL_ALIASES: dict[str, str] = {
46
+ "haiku": CHEAPEST_CLAUDE,
47
+ "sonnet": QUALITY_CLAUDE,
48
+ "ollama": CHEAPEST_OLLAMA,
49
+ "ollama:llama3": CHEAPEST_OLLAMA,
50
+ "ollama:qwen2.5": ALT_OLLAMA,
51
+ CHEAPEST_CLAUDE: CHEAPEST_CLAUDE,
52
+ QUALITY_CLAUDE: QUALITY_CLAUDE,
53
+ }
54
+
55
+ # Models this subsystem is allowed to dispatch. Anything resolving outside
56
+ # this set falls back to the cheapest safe default rather than dispatching
57
+ # an unvetted (or forbidden) model.
58
+ _ALLOWED: frozenset[str] = frozenset(_MODEL_ALIASES.values())
59
+
60
+
61
+ def _resolve_model_alias(alias: str) -> str:
62
+ """Translate a short alias (``haiku``/``sonnet``) to an allow-listed id.
63
+
64
+ Unknown aliases fall back to the cheapest Claude model — the same
65
+ fail-safe the evolver has always used.
66
+ """
67
+ return _MODEL_ALIASES.get(alias, CHEAPEST_CLAUDE)
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class ResolvedModels:
72
+ """Concrete allow-listed model id for each pipeline step."""
73
+
74
+ mutation: str
75
+ verify: str
76
+ confirm: str
77
+ independent: bool # True when verify != mutation (blind-verify is sound)
78
+
79
+
80
+ def _cheapest_for_backend(backend: str) -> str:
81
+ """Lowest-cost allow-listed model for a detected backend."""
82
+ if backend == "ollama":
83
+ return CHEAPEST_OLLAMA
84
+ # claude CLI, anthropic API, and any non-Ollama backend evolution can
85
+ # actually dispatch route through the cheapest Claude model.
86
+ return CHEAPEST_CLAUDE
87
+
88
+
89
+ def _resolve_field(value: str, default: str) -> str:
90
+ """Resolve a config field: empty → default; else alias→id, validated."""
91
+ if not value:
92
+ return default
93
+ resolved = _resolve_model_alias(value)
94
+ if resolved not in _ALLOWED: # pragma: no cover — defensive
95
+ logger.warning(
96
+ "evolution: configured model %r is not allow-listed; "
97
+ "falling back to %s", value, default,
98
+ )
99
+ return default
100
+ return resolved
101
+
102
+
103
+ def _independent_verifier(
104
+ mutation: str, *, ollama_available: bool,
105
+ ) -> str:
106
+ """Pick a cheap verifier that differs from the generator when possible.
107
+
108
+ - Ollama generator → the *other* local model (both free, distinct).
109
+ - Claude generator + local Ollama up → free local verifier.
110
+ - Claude generator, no Ollama → the cheapest *different* Claude tier if
111
+ the generator was a premium model; otherwise reuse the cheapest model
112
+ (caller flags reduced independence — we don't pay for a premium
113
+ verifier just to force distinctness).
114
+ """
115
+ if mutation.startswith("ollama:"):
116
+ return ALT_OLLAMA if mutation != ALT_OLLAMA else CHEAPEST_OLLAMA
117
+ if ollama_available:
118
+ return CHEAPEST_OLLAMA
119
+ if mutation != CHEAPEST_CLAUDE:
120
+ return CHEAPEST_CLAUDE
121
+ return CHEAPEST_CLAUDE
122
+
123
+
124
+ def resolve_evolution_models(
125
+ config: object, backend: str, *, ollama_available: bool,
126
+ ) -> ResolvedModels:
127
+ """Resolve mutation/verify/confirm models for a backend.
128
+
129
+ ``config`` is an ``EvolutionConfig`` (duck-typed: reads
130
+ ``mutation_model`` / ``verify_model`` / ``confirm_model`` attributes,
131
+ each an alias, allow-listed id, or "" for auto).
132
+ """
133
+ cheapest = _cheapest_for_backend(backend)
134
+
135
+ mutation = _resolve_field(getattr(config, "mutation_model", ""), cheapest)
136
+ confirm = _resolve_field(getattr(config, "confirm_model", ""), cheapest)
137
+
138
+ verify_cfg = getattr(config, "verify_model", "")
139
+ if verify_cfg:
140
+ verify = _resolve_field(verify_cfg, cheapest)
141
+ else:
142
+ verify = _independent_verifier(
143
+ mutation, ollama_available=ollama_available,
144
+ )
145
+
146
+ independent = verify != mutation
147
+ if not independent:
148
+ logger.warning(
149
+ "evolution: blind-verify model == generator model (%s) — "
150
+ "blind-verification independence is reduced. Set "
151
+ "evolution.verify_model to a different model, or run Ollama "
152
+ "locally, for an independent verifier.",
153
+ mutation,
154
+ )
155
+ return ResolvedModels(
156
+ mutation=mutation,
157
+ verify=verify,
158
+ confirm=confirm,
159
+ independent=independent,
160
+ )
@@ -89,6 +89,17 @@ def parse_mutation_output(output: str) -> Optional[str]:
89
89
  return None
90
90
 
91
91
 
92
+ # An evolved SKILL.md is auto-loaded into future sessions, so a mutation that
93
+ # hallucinated (or was prompt-injected into producing) code-execution or
94
+ # secret-exfiltration instructions must never be persisted. Structure checks
95
+ # alone are insufficient — reject dangerous content outright.
96
+ _SKILL_DENY_PATTERNS: tuple[str, ...] = (
97
+ "os.environ", "subprocess", "exec(", "eval(", "__import__", "import os",
98
+ "import subprocess", "pickle.loads", "curl ", "wget ", "rm -rf",
99
+ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "AWS_SECRET", "/.ssh/", ".install_token",
100
+ )
101
+
102
+
92
103
  def validate_skill_content(content: str) -> Optional[str]:
93
104
  """Validate evolved skill content. Returns error message or None if valid."""
94
105
  if not content or len(content) < 50:
@@ -97,6 +108,11 @@ def validate_skill_content(content: str) -> Optional[str]:
97
108
  return "Missing YAML frontmatter (no --- found)"
98
109
  if content.count("---") >= 2 and "name:" not in content.split("---")[1]:
99
110
  return "Missing 'name:' in frontmatter"
111
+ lowered = content.lower()
112
+ for pat in _SKILL_DENY_PATTERNS:
113
+ if pat.lower() in lowered:
114
+ return (f"Rejected: evolved skill contains a disallowed pattern "
115
+ f"({pat!r}) — skills must not execute code or access secrets")
100
116
  return None
101
117
 
102
118