superlocalmemory 3.7.8 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  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/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +827 -72
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -283,22 +283,22 @@ class EntityResolver:
283
283
  entity = self._db.get_entity_by_name(name, profile_id)
284
284
  if entity is not None:
285
285
  resolution[raw] = entity.entity_id
286
- self._touch_last_seen(entity.entity_id)
286
+ self._touch_last_seen(entity.entity_id, profile_id)
287
287
  continue
288
288
 
289
289
  # Tier b: alias match (case-insensitive, indexed)
290
290
  entity_id = self._alias_lookup(name, profile_id)
291
291
  if entity_id is not None:
292
292
  resolution[raw] = entity_id
293
- self._touch_last_seen(entity_id)
293
+ self._touch_last_seen(entity_id, profile_id)
294
294
  continue
295
295
 
296
296
  # Tier c: fuzzy match via Jaro-Winkler
297
297
  match_id, score = self._fuzzy_match(name, profile_id)
298
298
  if match_id is not None and score >= JARO_WINKLER_AUTO_MERGE:
299
299
  resolution[raw] = match_id
300
- self._persist_alias(match_id, name, score, "jaro_winkler")
301
- self._touch_last_seen(match_id)
300
+ self._persist_alias(match_id, name, score, "jaro_winkler", profile_id)
301
+ self._touch_last_seen(match_id, profile_id)
302
302
  continue
303
303
 
304
304
  # Candidate zone (0.70–0.85): queue for LLM in Mode B/C
@@ -384,7 +384,7 @@ class EntityResolver:
384
384
  and removes the merged entity record.
385
385
  """
386
386
  # Move aliases from merge -> keep
387
- aliases = self._db.get_aliases_for_entity(entity_id_merge)
387
+ aliases = self._db.get_aliases_for_entity(entity_id_merge, profile_id)
388
388
  for alias in aliases:
389
389
  new_alias = EntityAlias(
390
390
  alias_id=_new_id(),
@@ -393,7 +393,7 @@ class EntityResolver:
393
393
  confidence=alias.confidence,
394
394
  source=f"merge_from:{entity_id_merge}",
395
395
  )
396
- self._db.store_alias(new_alias)
396
+ self._db.store_alias(new_alias, profile_id)
397
397
 
398
398
  # Also add the merged entity's canonical name as an alias of keep
399
399
  merged = self._db.get_entity_by_name("", "") # placeholder
@@ -403,7 +403,7 @@ class EntityResolver:
403
403
  )
404
404
  if rows:
405
405
  merged_name = str(dict(rows[0])["canonical_name"])
406
- self._persist_alias(entity_id_keep, merged_name, 1.0, "merge")
406
+ self._persist_alias(entity_id_keep, merged_name, 1.0, "merge", profile_id)
407
407
 
408
408
  # Update atomic_facts: replace entity_id_merge with entity_id_keep
409
409
  # in canonical_entities_json column
@@ -435,14 +435,14 @@ class EntityResolver:
435
435
  except (json.JSONDecodeError, TypeError):
436
436
  continue
437
437
 
438
- # Delete the merged entity
438
+ # Delete the merged entity (tenant-scoped)
439
439
  self._db.execute(
440
- "DELETE FROM entity_aliases WHERE entity_id = ?",
441
- (entity_id_merge,),
440
+ "DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
441
+ (entity_id_merge, profile_id),
442
442
  )
443
443
  self._db.execute(
444
- "DELETE FROM canonical_entities WHERE entity_id = ?",
445
- (entity_id_merge,),
444
+ "DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
445
+ (entity_id_merge, profile_id),
446
446
  )
447
447
  logger.info(
448
448
  "Merged entity %s into %s (profile=%s)",
@@ -526,7 +526,7 @@ class EntityResolver:
526
526
  self._db.store_entity(entity)
527
527
 
528
528
  # Store name as its own alias for uniform lookup
529
- self._persist_alias(entity.entity_id, name, 1.0, "canonical")
529
+ self._persist_alias(entity.entity_id, name, 1.0, "canonical", profile_id)
530
530
 
531
531
  logger.debug(
532
532
  "Created entity '%s' [%s] (type=%s, profile=%s)",
@@ -540,13 +540,16 @@ class EntityResolver:
540
540
  alias_text: str,
541
541
  confidence: float,
542
542
  source: str,
543
+ profile_id: str,
543
544
  ) -> None:
544
- """Store an alias, skipping duplicates."""
545
- # Check if alias already exists for this entity
545
+ """Store an alias under a profile, skipping duplicates within it."""
546
+ # Check if alias already exists for this entity IN THIS PROFILE. The
547
+ # dedup must be profile-scoped or one profile's alias suppresses
548
+ # another profile's identical alias for a same-id entity.
546
549
  existing = self._db.execute(
547
550
  "SELECT alias_id FROM entity_aliases "
548
- "WHERE entity_id = ? AND LOWER(alias) = LOWER(?)",
549
- (entity_id, alias_text),
551
+ "WHERE entity_id = ? AND profile_id = ? AND LOWER(alias) = LOWER(?)",
552
+ (entity_id, profile_id, alias_text),
550
553
  )
551
554
  if existing:
552
555
  return
@@ -557,13 +560,20 @@ class EntityResolver:
557
560
  confidence=confidence,
558
561
  source=source,
559
562
  )
560
- self._db.store_alias(alias)
563
+ self._db.store_alias(alias, profile_id)
561
564
 
562
- def _touch_last_seen(self, entity_id: str) -> None:
563
- """Update last_seen timestamp on a canonical entity."""
565
+ def _touch_last_seen(self, entity_id: str, profile_id: str = "default") -> None:
566
+ """Update last_seen timestamp on a canonical entity scoped to profile.
567
+
568
+ L-01 fix: the original query had no profile_id guard. Since entity_ids are
569
+ UUIDs today the blast radius is theoretical, but the guard is required for
570
+ defense-in-depth against future import/sharing features that could introduce
571
+ UUID collisions across profiles.
572
+ """
564
573
  self._db.execute(
565
- "UPDATE canonical_entities SET last_seen = ? WHERE entity_id = ?",
566
- (_now(), entity_id),
574
+ "UPDATE canonical_entities SET last_seen = ? "
575
+ "WHERE entity_id = ? AND profile_id = ?",
576
+ (_now(), entity_id, profile_id),
567
577
  )
568
578
 
569
579
  # -- Internal: LLM disambiguation (Mode B/C) ---------------------------
@@ -621,9 +631,9 @@ class EntityResolver:
621
631
  entity_id = known[name_str]
622
632
  resolved[mention_str] = entity_id
623
633
  self._persist_alias(
624
- entity_id, mention_str, 0.9, "llm",
634
+ entity_id, mention_str, 0.9, "llm", profile_id,
625
635
  )
626
- self._touch_last_seen(entity_id)
636
+ self._touch_last_seen(entity_id, profile_id)
627
637
  # If LLM says it's itself, leave for caller to create
628
638
  return resolved
629
639
 
@@ -685,11 +685,36 @@ class FactExtractor:
685
685
  temperature=0.0,
686
686
  max_tokens=1024,
687
687
  )
688
- return self._parse_llm_response(raw, session_id, session_date)
688
+ facts = self._parse_llm_response(raw, session_id, session_date)
689
+ return self._reflexion_refine(conversation_text, facts)
689
690
  except Exception as exc:
690
691
  logger.warning("LLM fact extraction failed: %s", exc)
691
692
  return []
692
693
 
694
+ def _reflexion_refine(
695
+ self, source_text: str, facts: list[AtomicFact],
696
+ ) -> list[AtomicFact]:
697
+ """Wave Q1: bounded, fail-open entity self-review (Mode B/C only).
698
+
699
+ Reached only from the LLM extraction path, so Mode A never runs this.
700
+ Any failure returns the facts unchanged.
701
+ """
702
+ if not facts or not getattr(
703
+ self._config, "enable_entity_reflexion", True,
704
+ ):
705
+ return facts
706
+ try:
707
+ from superlocalmemory.encoding.entity_reflexion import EntityReflexion
708
+
709
+ reflexion = EntityReflexion(
710
+ self._llm,
711
+ max_facts=getattr(self._config, "reflexion_max_facts", 8),
712
+ )
713
+ return reflexion.refine(source_text, facts)
714
+ except Exception as exc:
715
+ logger.debug("Entity reflexion skipped: %s", exc)
716
+ return facts
717
+
693
718
  def _parse_llm_response(
694
719
  self,
695
720
  raw: str,
@@ -96,6 +96,18 @@ class TemporalValidator:
96
96
  severity = contradiction["severity"]
97
97
  reason = contradiction["description"]
98
98
 
99
+ # Temporal-anchor guard: if the new fact and the old fact carry
100
+ # distinct explicit event anchors, they describe the same subject at
101
+ # different times (a valid timeline — "lived in Delhi (2020)" vs
102
+ # "lives in Mumbai (2024)"), NOT a contradiction. Do not supersede;
103
+ # both stay recallable so historical queries still find them.
104
+ if self._is_historical_progression(new_fact, old_fact_id):
105
+ logger.debug(
106
+ "Temporal: %s vs %s have distinct event anchors — historical "
107
+ "progression, not superseding", new_fact.fact_id, old_fact_id,
108
+ )
109
+ continue
110
+
99
111
  # Step 1: Invalidate the old fact (bi-temporal)
100
112
  self.invalidate_fact(
101
113
  fact_id=old_fact_id,
@@ -119,6 +131,54 @@ class TemporalValidator:
119
131
  )
120
132
  return actions
121
133
 
134
+ @staticmethod
135
+ def _event_anchor(
136
+ referenced_date: str | None,
137
+ observation_date: str | None,
138
+ interval_start: str | None,
139
+ ) -> str | None:
140
+ """The most-specific explicit event time, or None when undated."""
141
+ return referenced_date or observation_date or interval_start or None
142
+
143
+ def _is_historical_progression(
144
+ self, new_fact: AtomicFact, old_fact_id: str,
145
+ ) -> bool:
146
+ """True when new_fact and the old fact have distinct explicit event
147
+ anchors (same subject, different times) — a valid timeline rather than a
148
+ contradiction. Conservative: False unless BOTH anchors are present and
149
+ resolve to different calendar days, so undated facts supersede as before.
150
+ """
151
+ new_anchor = self._event_anchor(
152
+ getattr(new_fact, "referenced_date", None),
153
+ getattr(new_fact, "observation_date", None),
154
+ getattr(new_fact, "interval_start", None),
155
+ )
156
+ if not new_anchor:
157
+ return False
158
+ try:
159
+ rows = self._db.execute(
160
+ "SELECT referenced_date, observation_date, interval_start "
161
+ "FROM atomic_facts WHERE fact_id = ?",
162
+ (old_fact_id,),
163
+ )
164
+ except Exception:
165
+ return False
166
+ if not rows:
167
+ return False
168
+ d = dict(rows[0])
169
+ old_anchor = self._event_anchor(
170
+ d.get("referenced_date"), d.get("observation_date"),
171
+ d.get("interval_start"),
172
+ )
173
+ if not old_anchor:
174
+ return False
175
+ from superlocalmemory.retrieval.time_window import parse_timestamp
176
+ nd = parse_timestamp(new_anchor)
177
+ od = parse_timestamp(old_anchor)
178
+ if nd is None or od is None:
179
+ return False
180
+ return nd.date() != od.date()
181
+
122
182
  def detect_contradiction(
123
183
  self,
124
184
  new_fact: AtomicFact,
@@ -206,7 +266,10 @@ class TemporalValidator:
206
266
  Both params are REQUIRED in the call site. Do NOT rename to is_valid().
207
267
  """
208
268
  try:
209
- tv = self._db.get_temporal_validity(fact_id)
269
+ # profile_id or None: an empty-string default must NOT become a
270
+ # `profile_id = ''` filter (which never matches → an expired fact
271
+ # would be reported valid). None falls back to the unscoped lookup.
272
+ tv = self._db.get_temporal_validity(fact_id, profile_id or None)
210
273
  if tv is None:
211
274
  return True # No temporal record = assumed valid
212
275
  return (
@@ -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
  # ---------------------------------------------------------------------------