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
@@ -0,0 +1,52 @@
1
+ """Pure decision predicates for the bounded-loop engine.
2
+
3
+ Every function here is a pure function of its arguments: same inputs give the
4
+ same output, and nothing is mutated. The engine calls these to decide whether
5
+ to stop, halt, or keep going; adapters never call them directly.
6
+
7
+ Imports are limited to the standard library and the loop models.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Sequence
13
+
14
+ from superlocalmemory.loops.models import Bounds, Rung, Verdict
15
+
16
+
17
+ def stop_condition_met(verdict: Verdict) -> bool:
18
+ """Return True when the gate verdict means the loop may exit.
19
+
20
+ The rule is deliberately conservative: a loop is eligible to finish only
21
+ when the independent gate reports ``passed``. The agent's own opinion is
22
+ never consulted here — it is not even an argument.
23
+ """
24
+ return verdict.passed
25
+
26
+
27
+ def no_progress(lap_changes: Sequence[bool], window: int) -> bool:
28
+ """Return True when the last ``window`` laps all made no change.
29
+
30
+ ``lap_changes`` is the ordered history of each lap's ``changed`` flag,
31
+ most-recent last. A window of ``0`` disables the check (a spinning agent
32
+ is then bounded only by the iteration cap). Fewer laps than the window
33
+ means "not enough evidence yet" and returns False.
34
+ """
35
+ if window <= 0:
36
+ return False
37
+ tail = lap_changes[-window:]
38
+ if len(tail) < window:
39
+ return False
40
+ return all(changed is False for changed in tail)
41
+
42
+
43
+ def rung_requires_approval(rung: Rung, bounds: Bounds) -> bool:
44
+ """Return True when a human must approve a passing gate before DONE.
45
+
46
+ An explicit ``bounds.require_approval`` wins outright. When it is ``None``
47
+ the posture is derived from the rung: L1 exits without approval, while
48
+ L2 and L3 require it.
49
+ """
50
+ if bounds.require_approval is not None:
51
+ return bounds.require_approval
52
+ return rung in (Rung.L2, Rung.L3)
@@ -43,6 +43,7 @@ class DaemonPoolProxy:
43
43
  fast: bool = False,
44
44
  include_global: bool | None = None,
45
45
  include_shared: bool | None = None,
46
+ window: str | None = None,
46
47
  ) -> dict[str, Any]:
47
48
  _params: dict[str, Any] = {
48
49
  "q": query,
@@ -57,6 +58,8 @@ class DaemonPoolProxy:
57
58
  _params["include_global"] = "true" if include_global else "false"
58
59
  if include_shared is not None:
59
60
  _params["include_shared"] = "true" if include_shared else "false"
61
+ if window:
62
+ _params["window"] = window
60
63
  params = urllib.parse.urlencode(_params)
61
64
  try:
62
65
  from superlocalmemory.cli.daemon import daemon_request
@@ -104,6 +104,8 @@ def pool_recall(query: str, limit: int = 10, **kwargs: Any) -> PoolRecallRespons
104
104
  _recall_kwargs["include_global"] = kwargs["include_global"]
105
105
  if "include_shared" in kwargs:
106
106
  _recall_kwargs["include_shared"] = kwargs["include_shared"]
107
+ if kwargs.get("window"):
108
+ _recall_kwargs["window"] = kwargs["window"]
107
109
  raw = _pool().recall(**_recall_kwargs)
108
110
  _unwrap_error(raw, "recall")
109
111
  items = raw.get("results", []) if isinstance(raw, dict) else []
@@ -0,0 +1,103 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """MCP profile definitions — pure data, no side effects.
6
+
7
+ Extracted from mcp/server.py (v3.8.0) so the daemon can import profile
8
+ metadata without triggering FastMCP tool registration or engine warmup.
9
+
10
+ server.py re-exports all names from this module for backward compatibility.
11
+ Do NOT import FastMCP, MemoryEngine, or any heavy dependency here.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # v3.6.14 WP-01: Named profile definitions
18
+ # ---------------------------------------------------------------------------
19
+
20
+ _PROFILE_CORE: frozenset[str] = frozenset({ # 14
21
+ "remember", "recall", "search", "fetch", "list_recent", "update_memory", "forget",
22
+ "session_init", "close_session",
23
+ "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
24
+ })
25
+
26
+ _PROFILE_CODE: frozenset[str] = _PROFILE_CORE | frozenset({ # 24
27
+ "build_code_graph", "get_blast_radius", "query_graph",
28
+ "semantic_search_code", "get_review_context", "detect_changes",
29
+ # switch_profile lets a plugin/IDE session change the active workspace over
30
+ # MCP (the plugin ships SLM_MCP_PROFILE=code, so it must be here). The
31
+ # underlying route is RBAC member-gated, so company-mode isolation holds.
32
+ "switch_profile",
33
+ # v3.8.0: bounded loops on the MCP surface. Coding agents (the /slm-loop
34
+ # command's audience) run gated, bounded loops and inspect the ledger.
35
+ "slm_loop_run", "slm_loop_history", "slm_loop_show",
36
+ })
37
+
38
+ _PROFILE_FULL_MESH: frozenset[str] = frozenset({ # 8
39
+ "mesh_summary", "mesh_peers", "mesh_send", "mesh_inbox",
40
+ "mesh_state", "mesh_lock", "mesh_events", "mesh_status",
41
+ })
42
+
43
+ _PROFILE_FULL: frozenset[str] = frozenset({ # 34 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
44
+ "remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
45
+ "get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
46
+ "run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
47
+ "log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
48
+ "evolve_skill", "skill_health", "skill_lineage", "switch_profile",
49
+ "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
50
+ # v3.8.0: bounded-loop tools (CLI + /slm-loop command + MCP).
51
+ "slm_loop_run", "slm_loop_history", "slm_loop_show",
52
+ }) | _PROFILE_FULL_MESH # 42
53
+
54
+ _PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 54
55
+ "get_version", "get_mode", "health", "consistency_check", "recall_trace",
56
+ "get_lifecycle_status", "set_retention_policy", "compact_memories",
57
+ "get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
58
+ })
59
+
60
+ _PROFILE_MESH: frozenset[str] = _PROFILE_FULL_MESH # 8
61
+
62
+ # Canonical name → frozenset mapping. "whole" is intentionally absent —
63
+ # it maps to the raw server (all tools, D-2 LOCKED).
64
+ _PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
65
+ "core": _PROFILE_CORE,
66
+ "code": _PROFILE_CODE,
67
+ "full": _PROFILE_FULL,
68
+ "power": _PROFILE_POWER,
69
+ "mesh": _PROFILE_MESH,
70
+ }
71
+
72
+ # Compatibility aliases published by the v3.6 README. Stale client
73
+ # configurations have one deterministic meaning; migration warnings fire
74
+ # at server startup. Any other value is a configuration error (fail closed).
75
+ _PROFILE_ALIASES: dict[str, str] = {
76
+ "core14": "core",
77
+ # 3.8.0: switch_profile (+1) then bounded-loop tools (+3) grew code/full/
78
+ # power. Every historical count-suffixed name is kept so a v3.6/3.7/early-
79
+ # 3.8 config still resolves (back-compat); new 3.8.0 counts added alongside.
80
+ "code20": "code",
81
+ "code21": "code",
82
+ "code24": "code",
83
+ "full38": "full",
84
+ "full39": "full",
85
+ "full42": "full",
86
+ "power50": "power",
87
+ "power51": "power",
88
+ "power54": "power",
89
+ "mesh8": "mesh",
90
+ "whole81": "whole",
91
+ "whole84": "whole",
92
+ }
93
+
94
+ # Plain-English descriptions for UI display.
95
+ # Rules: no internal jargon (no POMDP, Fisher-Rao, TurboQuant, etc.),
96
+ # one sentence, user-facing language only.
97
+ PROFILE_DESCRIPTIONS: dict[str, str] = {
98
+ "core": "Essential memory: store, recall, search, sessions",
99
+ "code": "Core + code-graph tools + profile switching (default for IDE coding agents)",
100
+ "full": "All everyday memory, optimization, and mesh tools",
101
+ "power": "Everything in full plus advanced governance/behavioral tools",
102
+ "mesh": "Cross-device mesh coordination only",
103
+ }
@@ -75,13 +75,13 @@ def reset_engine():
75
75
 
76
76
  # Register tools and resources -------------------------------------------------
77
77
  #
78
- # Essential-only default: 25 base tools + 8 mesh tools = 33 registered
78
+ # Essential-only default: 34 base tools + 8 mesh tools = 42 registered
79
79
  # when mesh is enabled. Set ``SLM_MCP_ALL_TOOLS=1`` to expose the full
80
80
  # toolset. Rationale: IDEs cap at 50-100 tools total (Cursor,
81
81
  # Antigravity, Windsurf) and a maximal SLM registration crowds out
82
82
  # other MCP servers the user may have installed.
83
83
  # Admin/diagnostics tools remain available via CLI (`slm <command>`).
84
- # Set SLM_MCP_ALL_TOOLS=1 to enable all 38 tools (power users).
84
+ # Set SLM_MCP_ALL_TOOLS=1 to enable all 84 tools (power users).
85
85
 
86
86
  import os as _os_reg
87
87
 
@@ -107,6 +107,10 @@ _ESSENTIAL_TOOLS: set[str] = {
107
107
  "evolve_skill", "skill_health", "skill_lineage",
108
108
  # v3.6.11: Surface B Optimize tools (5)
109
109
  "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
110
+ # v3.8.0: bounded-loop tools (3) — CLI + /slm-loop command + MCP. Kept in
111
+ # the default exposure so any MCP client discovers gated loops, not just
112
+ # profile=code/full/power sessions.
113
+ "slm_loop_run", "slm_loop_history", "slm_loop_show",
110
114
  }
111
115
 
112
116
  # v3.4.4: Mesh tools — enabled if mesh_enabled in config or SLM_MCP_MESH_TOOLS=1
@@ -138,55 +142,21 @@ _user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
138
142
 
139
143
  # ---------------------------------------------------------------------------
140
144
  # v3.6.14 WP-01: Named profile definitions
145
+ # Extracted to mcp/profiles.py (v3.8.0) — pure data, no side effects.
146
+ # All names re-exported here for backward compatibility with existing tests
147
+ # and any code that imports them from this module.
141
148
  # ---------------------------------------------------------------------------
142
149
 
143
- _PROFILE_CORE = frozenset({ # 14
144
- "remember", "recall", "search", "fetch", "list_recent", "update_memory", "forget",
145
- "session_init", "close_session",
146
- "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
147
- })
148
- _PROFILE_CODE = _PROFILE_CORE | frozenset({ # 20
149
- "build_code_graph", "get_blast_radius", "query_graph",
150
- "semantic_search_code", "get_review_context", "detect_changes",
151
- })
152
- _PROFILE_FULL_MESH = frozenset({ # 8
153
- "mesh_summary", "mesh_peers", "mesh_send", "mesh_inbox",
154
- "mesh_state", "mesh_lock", "mesh_events", "mesh_status",
155
- })
156
- _PROFILE_FULL = frozenset({ # 30 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
157
- "remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
158
- "get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
159
- "run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
160
- "log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
161
- "evolve_skill", "skill_health", "skill_lineage",
162
- "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
163
- }) | _PROFILE_FULL_MESH # 38
164
- _PROFILE_POWER = _PROFILE_FULL | frozenset({ # 50
165
- "get_version", "get_mode", "health", "consistency_check", "recall_trace",
166
- "get_lifecycle_status", "set_retention_policy", "compact_memories",
167
- "get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
168
- })
169
- _PROFILE_MESH = _PROFILE_FULL_MESH # 8
170
-
171
- _PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
172
- "core": _PROFILE_CORE,
173
- "code": _PROFILE_CODE,
174
- "full": _PROFILE_FULL,
175
- "power": _PROFILE_POWER,
176
- "mesh": _PROFILE_MESH,
177
- } # "whole" intentionally absent — maps to raw server (D-2 LOCKED)
178
-
179
- # Compatibility aliases published by the v3.6 README. Keep these explicit so
180
- # a stale client configuration has one deterministic meaning and emits a
181
- # migration warning. Any other value is a configuration error (fail closed).
182
- _PROFILE_ALIASES: dict[str, str] = {
183
- "core14": "core",
184
- "code20": "code",
185
- "full38": "full",
186
- "power50": "power",
187
- "mesh8": "mesh",
188
- "whole81": "whole",
189
- }
150
+ from superlocalmemory.mcp.profiles import ( # noqa: E402 (after env setups above)
151
+ _PROFILE_CORE,
152
+ _PROFILE_CODE,
153
+ _PROFILE_FULL_MESH,
154
+ _PROFILE_FULL,
155
+ _PROFILE_POWER,
156
+ _PROFILE_MESH,
157
+ _PROFILE_DEFINITIONS,
158
+ _PROFILE_ALIASES,
159
+ )
190
160
 
191
161
  _profile = _os_reg.environ.get("SLM_MCP_PROFILE", "").strip().lower()
192
162
 
@@ -233,7 +203,7 @@ class _FilteredServer:
233
203
  """
234
204
  __slots__ = ("_server", "_allowed")
235
205
 
236
- def __init__(self, real_server: FastMCP, allowed: frozenset[str]) -> None:
206
+ def __init__(self, real_server: SLMFastMCP, allowed: frozenset[str]) -> None:
237
207
  self._server = real_server
238
208
  self._allowed = allowed
239
209
 
@@ -291,31 +261,24 @@ register_learning_tools(_target, get_engine) # v3.4.7: Two-way learning tools
291
261
  register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
292
262
  from superlocalmemory.mcp.tools_optimize import register_optimize_tools
293
263
  register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-free)
264
+ from superlocalmemory.mcp.tools_loops import register_loop_tools
265
+ register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
294
266
 
295
267
 
296
- # V3.3.21: Eager engine warmup start initializing BEFORE first tool call.
297
- # The MCP server process starts when the IDE launches. Previously, the engine
298
- # was lazy-loaded on first tool call 23s cold start for the user.
299
- # Now: engine starts warming in a background thread immediately. By the time
300
- # the first tool call arrives (1-2s later), the engine is already warm.
301
- # This applies to ALL IDEs: Claude Code, Cursor, Antigravity, Gemini CLI, etc.
268
+ # Keep stdio MCP processes thin until a tool truly needs a local LIGHT engine.
269
+ # Every open IDE/task owns a stdio process; eagerly opening memory.db in all of
270
+ # them multiplied RAM and SQLite writers on machines with many long-lived
271
+ # sessions. The shared daemon owns model warmup and common remember/recall work.
302
272
  def _eager_warmup() -> None:
303
- """Pre-warm LIGHT engine + ensure daemon is running + auto-register mesh.
273
+ """Ensure the shared daemon is running without opening a per-client engine.
304
274
 
305
- LIGHT engine init is cheap (DB only, ~100 ms). The real reason this
306
- stays in a background thread is the follow-on side effects
307
- (``ensure_daemon``, ``auto_register_mesh``) which do I/O.
275
+ Mesh registration is intentionally lazy: a local stdio session is not a
276
+ remote peer, and heartbeat writes should begin only after a mesh tool is
277
+ actually used.
308
278
  """
309
- import logging
310
279
  _logger = logging.getLogger(__name__)
311
- try:
312
- get_engine()
313
- _logger.info("MCP engine pre-warmed successfully")
314
- except Exception as exc:
315
- _logger.warning("MCP engine pre-warmup failed: %s", exc)
316
280
 
317
- # Measurement / test harnesses set this to skip daemon-start and
318
- # mesh-register. The LIGHT engine init above still runs.
281
+ # Measurement / test harnesses set this to skip daemon-start.
319
282
  if _os.environ.get("SLM_DISABLE_WARMUP_SIDE_EFFECTS") == "1":
320
283
  return
321
284
 
@@ -328,22 +291,12 @@ def _eager_warmup() -> None:
328
291
  except Exception as exc:
329
292
  _logger.warning("Daemon auto-start failed: %s", exc)
330
293
 
331
- # V3.4.6: Auto-register this MCP session as a mesh peer immediately.
332
- # Previously, registration was lazy (only on first mesh tool call).
333
- # Now every Claude session appears on the mesh from startup.
334
- try:
335
- from superlocalmemory.mcp.tools_mesh import auto_register_mesh
336
- auto_register_mesh()
337
- _logger.info("Mesh peer auto-registered at startup")
338
- except Exception as exc:
339
- _logger.warning("Mesh auto-register failed: %s", exc)
340
-
341
294
  import threading
342
295
 
343
296
  # v3.6.7: Suppress standalone-process behaviours when the MCP server is
344
297
  # imported inside the daemon (SLM_MCP_EMBEDDED=1). Three threads are safe
345
298
  # to run in a dedicated `slm mcp` subprocess but harmful inside the daemon:
346
- # mcp-warmup — creates a LIGHT engine duplicate; daemon has a FULL one.
299
+ # mcp-warmup — ensures the shared daemon only; never creates an engine.
347
300
  # parent-watchdog — calls os._exit(0) if its parent IDE quits, which would
348
301
  # kill the daemon along with it.
349
302
  # stdin-eof-monitor — monitors stdin pipe; meaningless inside the daemon.
@@ -186,12 +186,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
186
186
  relevance score is ≥ 0.70 (architectural decisions that remain
187
187
  permanently relevant still surface). Default: 30.
188
188
  Set to 0 to disable the age gate entirely.
189
-
190
- Scoring: five candidate producers (semantic + BM25 + temporal +
191
- spreading_activation + hopfield) feed RRF fusion; the entity graph then
192
- applies an optional post-fusion score enhancement. Combined with
193
- Ebbinghaus exponential recency decay and FSRS stability strengthening by
194
- access frequency.
195
189
  """
196
190
  try:
197
191
  from superlocalmemory.hooks.rules_engine import RulesEngine
@@ -689,7 +683,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
689
683
  try:
690
684
  engine = get_engine()
691
685
  db = engine.db
692
- pid = profile_id or engine.profile_id
686
+ # Isolation: the tenant is ALWAYS the engine's active profile. The
687
+ # caller-supplied profile_id is ignored — honoring it let any MCP
688
+ # client read/pin another profile's facts by passing profile_id.
689
+ pid = engine.profile_id
693
690
 
694
691
  if action == "pin":
695
692
  if not fact_id:
@@ -14,10 +14,16 @@ All tools return {"success": bool, ...} envelope. Never raise.
14
14
  from __future__ import annotations
15
15
 
16
16
  import logging
17
+ import os
17
18
  import time
18
19
  from pathlib import Path
19
20
  from typing import Any, Callable
20
21
 
22
+ from superlocalmemory.core.security_primitives import (
23
+ PathTraversalError,
24
+ safe_resolve,
25
+ )
26
+
21
27
  logger = logging.getLogger(__name__)
22
28
 
23
29
  # ---------------------------------------------------------------------------
@@ -118,7 +124,14 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
118
124
  exclude_patterns: Comma-separated glob patterns to exclude.
119
125
  """
120
126
  try:
121
- repo = Path(repo_path)
127
+ # SEC: contain the untrusted repo_path under $HOME (same guard as
128
+ # update_code_graph) so repo_path="/" cannot ingest the whole
129
+ # filesystem into the code-graph DB.
130
+ from superlocalmemory.core.security_primitives import PathTraversalError
131
+ try:
132
+ repo = safe_resolve(Path.home(), repo_path)
133
+ except (PathTraversalError, OSError, ValueError) as exc:
134
+ return _error_response(f"Invalid repo_path: {exc}")
122
135
  if not repo.exists():
123
136
  return _error_response(
124
137
  f"Repository path does not exist: {repo_path}"
@@ -250,6 +263,22 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
250
263
  svc = _get_service()
251
264
  db = svc.db
252
265
 
266
+ # SEC 3.7.9 (B1): contain untrusted repo_path under $HOME and confirm
267
+ # it is a real git repo BEFORE any git subprocess runs with it as cwd.
268
+ # Blocks RCE via attacker-controlled .git config/hooks (core.fsmonitor,
269
+ # core.hooksPath) that git would otherwise execute in a hostile repo.
270
+ if repo_path:
271
+ try:
272
+ repo = safe_resolve(Path.home(), repo_path)
273
+ except PathTraversalError as exc:
274
+ return _error_response(f"Invalid repo_path: {exc}")
275
+ if not (repo / ".git").exists():
276
+ return _error_response(
277
+ f"Not a git repository (no .git dir): {repo_path}"
278
+ )
279
+ else:
280
+ repo = svc.config.repo_root
281
+
253
282
  files_list = [
254
283
  f.strip() for f in changed_files.split(",") if f.strip()
255
284
  ] if changed_files else []
@@ -258,11 +287,22 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
258
287
  # Auto-detect via git
259
288
  try:
260
289
  import subprocess
261
- repo = Path(repo_path) if repo_path else svc.config.repo_root
290
+ # repo is the safe_resolve-contained path computed above.
291
+ # Harden git: disable hooks + fsmonitor + system config so a
292
+ # hostile repo cannot execute code via the git invocation (B1).
262
293
  result = subprocess.run(
263
- ["git", "diff", "--name-only", "HEAD~1", "HEAD"],
294
+ [
295
+ "git", "-c", "core.hooksPath=/dev/null",
296
+ "-c", "core.fsmonitor=",
297
+ "diff", "--name-only", "HEAD~1", "HEAD",
298
+ ],
264
299
  capture_output=True, text=True, timeout=30,
265
300
  cwd=str(repo),
301
+ env={
302
+ **os.environ,
303
+ "GIT_CONFIG_NOSYSTEM": "1",
304
+ "GIT_TERMINAL_PROMPT": "0",
305
+ },
266
306
  )
267
307
  files_list = [
268
308
  f.strip() for f in result.stdout.strip().split("\n")
@@ -289,13 +329,19 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
289
329
  config = svc.config
290
330
  parser = CodeParser(config)
291
331
  store = GraphStore(db)
292
- repo = Path(repo_path) if repo_path else config.repo_root
332
+ # repo already contained via safe_resolve above (B1).
293
333
 
294
334
  nodes_before = db.get_node_count()
295
335
  edges_before = db.get_edge_count()
296
336
 
297
337
  for fp in files_list:
298
- full = repo / fp
338
+ # fp comes from caller-supplied changed_files; contain it under
339
+ # the (already safe-resolved) repo so "../../.ssh/id_rsa" cannot
340
+ # be read into the graph.
341
+ try:
342
+ full = safe_resolve(repo, fp)
343
+ except Exception:
344
+ continue
299
345
  if not full.exists():
300
346
  store.remove_file(fp)
301
347
  continue
@@ -84,8 +84,9 @@ def _record_recall_hits(
84
84
  enqueue_shown_flip,
85
85
  )
86
86
 
87
- engine = get_engine()
88
- pid = profile_id or engine.profile_id
87
+ pid = profile_id
88
+ if not pid:
89
+ pid = get_engine().profile_id
89
90
  slm_dir = canonical_data_root()
90
91
 
91
92
  shown_ids = [r.get("fact_id", "") for r in results[:10]
@@ -121,7 +122,7 @@ def _record_recall_hits(
121
122
  def register_core_tools(server, get_engine: Callable) -> None:
122
123
  """Register the 13 core MCP tools on *server*."""
123
124
 
124
- @server.tool(annotations=ToolAnnotations(idempotentHint=True))
125
+ @server.tool()
125
126
  async def remember(
126
127
  content: str, tags: str = "", project: str = "",
127
128
  importance: int = 5, session_id: str = "",
@@ -284,10 +285,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
284
285
  session_id: str = "", fast: bool = False,
285
286
  include_global: bool | None = None,
286
287
  include_shared: bool | None = None,
288
+ window: str = "",
287
289
  ) -> dict:
288
290
  """Search memories through hybrid retrieval, RRF fusion, and reranking.
289
291
 
290
- S9-DASH-02: optional ``session_id`` threads through to the
292
+ Optional ``session_id`` threads through to the
291
293
  engine's outcome-queue so PostToolUse / Stop hooks can attach
292
294
  engagement signals to this recall. Claude Code should pass its
293
295
  ``CLAUDE_SESSION_ID``. Omitting it degrades to "no closed-loop
@@ -297,6 +299,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
297
299
  scopes participate in retrieval. Leave them unset (``None``) to use the
298
300
  configured default — shared memory is OPT-IN, so by default recall
299
301
  returns only this profile's own facts. Pass ``True`` to opt in per call.
302
+
303
+ Time window: optional ``window`` restricts results to a event-time
304
+ range. Accepts a relative span (``"24h"``, ``"7d"``, ``"30d"``,
305
+ ``"1y"``) or an explicit range (``"2026-07-01..2026-07-31"``). Empty =
306
+ no time filter.
300
307
  """
301
308
  # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)
302
309
  if agent_id == "mcp_client":
@@ -353,6 +360,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
353
360
  fast=bool(fast),
354
361
  include_global=include_global,
355
362
  include_shared=include_shared,
363
+ window=window or None,
356
364
  )
357
365
  if result.get("ok"):
358
366
  # Record implicit feedback: every returned result is a recall_hit
@@ -98,11 +98,14 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
98
98
  engine = get_engine()
99
99
  profile_id = engine.profile_id if engine else "default"
100
100
 
101
- evolver._store.reset_cycle()
102
- outcome = evolver._process_candidate(candidate, profile_id)
101
+ evolver._store.reset_cycle(profile_id)
102
+ # audit-10 fix: go through evolve_candidate so the manual/MCP path
103
+ # runs under a budget cycle and honours the LLM-call / wall-time /
104
+ # per-day caps (previously _process_candidate ran uncapped here).
105
+ outcome = evolver.evolve_candidate(candidate, profile_id)
103
106
 
104
107
  # Fetch the latest record for this skill to return details
105
- recent = evolver._store.get_skill_history(skill_name, limit=1)
108
+ recent = evolver._store.get_skill_history(skill_name, profile_id, limit=1)
106
109
  record_info = {}
107
110
  if recent:
108
111
  r = recent[0]
@@ -143,7 +143,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
143
143
  logger.debug("get_assertions failed: %s", exc)
144
144
  return {"assertions": [], "count": 0, "error": str(exc)}
145
145
 
146
- @server.tool(annotations=ToolAnnotations(idempotentHint=True))
146
+ @server.tool()
147
147
  async def reinforce_assertion(assertion_id: str) -> dict:
148
148
  """Reinforce a behavioral assertion (increase confidence).
149
149
 
@@ -171,7 +171,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
171
171
  except Exception as exc:
172
172
  return {"success": False, "error": str(exc)}
173
173
 
174
- @server.tool(annotations=ToolAnnotations(idempotentHint=True))
174
+ @server.tool()
175
175
  async def contradict_assertion(assertion_id: str) -> dict:
176
176
  """Contradict a behavioral assertion (decrease confidence).
177
177