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
@@ -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
 
@@ -291,6 +261,8 @@ 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
268
  # V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.
@@ -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
@@ -284,10 +284,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
284
284
  session_id: str = "", fast: bool = False,
285
285
  include_global: bool | None = None,
286
286
  include_shared: bool | None = None,
287
+ window: str = "",
287
288
  ) -> dict:
288
289
  """Search memories through hybrid retrieval, RRF fusion, and reranking.
289
290
 
290
- S9-DASH-02: optional ``session_id`` threads through to the
291
+ Optional ``session_id`` threads through to the
291
292
  engine's outcome-queue so PostToolUse / Stop hooks can attach
292
293
  engagement signals to this recall. Claude Code should pass its
293
294
  ``CLAUDE_SESSION_ID``. Omitting it degrades to "no closed-loop
@@ -297,6 +298,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
297
298
  scopes participate in retrieval. Leave them unset (``None``) to use the
298
299
  configured default — shared memory is OPT-IN, so by default recall
299
300
  returns only this profile's own facts. Pass ``True`` to opt in per call.
301
+
302
+ Time window: optional ``window`` restricts results to a event-time
303
+ range. Accepts a relative span (``"24h"``, ``"7d"``, ``"30d"``,
304
+ ``"1y"``) or an explicit range (``"2026-07-01..2026-07-31"``). Empty =
305
+ no time filter.
300
306
  """
301
307
  # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)
302
308
  if agent_id == "mcp_client":
@@ -353,6 +359,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
353
359
  fast=bool(fast),
354
360
  include_global=include_global,
355
361
  include_shared=include_shared,
362
+ window=window or None,
356
363
  )
357
364
  if result.get("ok"):
358
365
  # Record implicit feedback: every returned result is a recall_hit
@@ -587,6 +594,10 @@ def register_core_tools(server, get_engine: Callable) -> None:
587
594
  )
588
595
 
589
596
  generation = 0
597
+ # Only the profile_id explicitly confirmed by this process
598
+ # (daemon-acknowledged + locally-validated, or locally
599
+ # validated directly) is ever synced into engine state.
600
+ confirmed_profile_id = None
590
601
  if await asyncio.to_thread(is_daemon_running):
591
602
  result = await asyncio.to_thread(
592
603
  daemon_request,
@@ -599,12 +610,35 @@ def register_core_tools(server, get_engine: Callable) -> None:
599
610
  "error": "resident daemon rejected the profile switch",
600
611
  }
601
612
  acknowledged = str(result.get("active_profile", ""))
602
- if acknowledged != profile_id:
613
+ if not acknowledged or acknowledged != profile_id:
603
614
  return {
604
615
  "success": False,
605
616
  "error": "resident daemon acknowledged a different profile",
606
617
  }
618
+ # Local consistency guard (SEC-H-01): the daemon's HTTP
619
+ # acknowledgement alone is not sufficient — this MCP
620
+ # process must also confirm the profile exists in its
621
+ # own local DB handle before syncing local state to it.
622
+ # Mirrors the existence check the no-daemon branch already
623
+ # performs below.
624
+ local_rows = engine._db.execute(
625
+ "SELECT 1 FROM profiles WHERE profile_id = ?",
626
+ (profile_id,),
627
+ )
628
+ if not local_rows:
629
+ return {
630
+ "success": False,
631
+ "error": (
632
+ f"resident daemon acknowledged profile "
633
+ f"'{acknowledged}' but it does not exist in "
634
+ f"this process's local profile store"
635
+ ),
636
+ }
607
637
  generation = int(result.get("generation", 0))
638
+ # Sync target is the DAEMON-CONFIRMED value, never the raw
639
+ # caller-supplied profile_id, even though they are equal
640
+ # here by construction (checked above).
641
+ confirmed_profile_id = acknowledged
608
642
  else:
609
643
  rows = engine._db.execute(
610
644
  "SELECT 1 FROM profiles WHERE profile_id = ?",
@@ -628,10 +662,21 @@ def register_core_tools(server, get_engine: Callable) -> None:
628
662
  engine._config.active_profile = old
629
663
  persistence.rollback()
630
664
  raise
665
+ confirmed_profile_id = profile_id
666
+
667
+ if not confirmed_profile_id:
668
+ # Defensive: should be unreachable — every path above
669
+ # either returns an error or sets confirmed_profile_id.
670
+ return {
671
+ "success": False,
672
+ "error": "profile switch could not be confirmed",
673
+ }
631
674
 
632
- # Synchronize this MCP process only after daemon acknowledgement.
633
- engine.profile_id = profile_id
634
- engine._config.active_profile = profile_id
675
+ # Synchronize this MCP process only after confirmation
676
+ # (daemon-acknowledged + locally-validated, or directly
677
+ # locally-validated in the no-daemon branch above).
678
+ engine.profile_id = confirmed_profile_id
679
+ engine._config.active_profile = confirmed_profile_id
635
680
 
636
681
  # v3.6.12 (search-3): recall/delete run in a separate worker
637
682
  # subprocess that caches its engine (and profile_id) at init. Recycle
@@ -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]