superlocalmemory 3.7.8 → 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 (260) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +69 -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 -1
  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 +94 -10
  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/backup.py +12 -1
  116. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  117. package/src/superlocalmemory/infra/data_root.py +43 -4
  118. package/src/superlocalmemory/infra/event_bus.py +107 -24
  119. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  120. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  121. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  122. package/src/superlocalmemory/learning/cross_project.py +28 -19
  123. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  124. package/src/superlocalmemory/loops/__init__.py +56 -0
  125. package/src/superlocalmemory/loops/budget.py +58 -0
  126. package/src/superlocalmemory/loops/engine.py +164 -0
  127. package/src/superlocalmemory/loops/ledger.py +243 -0
  128. package/src/superlocalmemory/loops/models.py +152 -0
  129. package/src/superlocalmemory/loops/rules.py +52 -0
  130. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  131. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  132. package/src/superlocalmemory/mcp/profiles.py +103 -0
  133. package/src/superlocalmemory/mcp/server.py +21 -49
  134. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  135. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  136. package/src/superlocalmemory/mcp/tools_core.py +8 -1
  137. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  138. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  139. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  140. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  141. package/src/superlocalmemory/mesh/broker.py +237 -129
  142. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  143. package/src/superlocalmemory/optimize/NOTICE +1 -6
  144. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  145. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  147. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  148. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  149. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  150. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  151. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  152. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  153. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  154. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  155. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  156. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  157. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  158. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  159. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  160. package/src/superlocalmemory/retrieval/engine.py +168 -26
  161. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  162. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  163. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  164. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  165. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  166. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  167. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  168. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  169. package/src/superlocalmemory/server/api.py +4 -4
  170. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  171. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  172. package/src/superlocalmemory/server/recall_health.py +24 -3
  173. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  174. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  175. package/src/superlocalmemory/server/routes/agents.py +128 -38
  176. package/src/superlocalmemory/server/routes/backup.py +34 -10
  177. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  178. package/src/superlocalmemory/server/routes/brain.py +21 -5
  179. package/src/superlocalmemory/server/routes/chat.py +10 -5
  180. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  181. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  183. package/src/superlocalmemory/server/routes/entity.py +9 -4
  184. package/src/superlocalmemory/server/routes/events.py +24 -8
  185. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  186. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  187. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  188. package/src/superlocalmemory/server/routes/insights.py +3 -3
  189. package/src/superlocalmemory/server/routes/learning.py +14 -14
  190. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  191. package/src/superlocalmemory/server/routes/memories.py +182 -57
  192. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  193. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  194. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  195. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  196. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  197. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  198. package/src/superlocalmemory/server/routes/stats.py +13 -6
  199. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  200. package/src/superlocalmemory/server/routes/v3_api.py +183 -69
  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 +384 -56
  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 +53 -0
  208. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  209. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  210. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  211. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  212. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  213. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  214. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  215. package/src/superlocalmemory/storage/models.py +4 -0
  216. package/src/superlocalmemory/storage/schema.py +87 -0
  217. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  218. package/src/superlocalmemory/trust/gate.py +49 -8
  219. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  220. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  221. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  222. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  223. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  224. package/src/superlocalmemory/ui/favicon.svg +35 -4
  225. package/src/superlocalmemory/ui/index.html +306 -173
  226. package/src/superlocalmemory/ui/js/brain.js +5 -20
  227. package/src/superlocalmemory/ui/js/core.js +47 -31
  228. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  229. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  230. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  231. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  232. package/src/superlocalmemory/ui/js/memories.js +15 -4
  233. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  234. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  235. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  236. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  237. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  238. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  239. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  240. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  241. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  242. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  243. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  244. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  245. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  246. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  247. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  248. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  249. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  250. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  251. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  252. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  253. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  254. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  255. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  256. package/src/superlocalmemory/ui/js/settings.js +2 -2
  257. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  258. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  259. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  260. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,267 @@
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
+ """Community summaries (Wave Q2) — one synthesized report per entity community.
6
+
7
+ Rides on the entity-community backbone (core.entity_community). For each
8
+ community it gathers the member entities' facts, EXCLUDES superseded facts
9
+ (bi-temporal — market CRIT-3), then produces:
10
+
11
+ - a keyword-dense signal (always, Mode A, zero-LLM), and
12
+ - a summary: Mode B/C LLM synthesis via the shared core.Summarizer
13
+ (which itself falls back to a heuristic), else the Mode A keyword-dense
14
+ line. Fail-open — a summarizer error never breaks generation.
15
+
16
+ Surfacing is on-device-safe: summaries are PRECOMPUTED here in the background
17
+ and later matched to a query as a single thematic-context block (Q2b) — never
18
+ a GraphRAG-style per-query LLM fan-out (market CRIT-1). member_fact_ids gives
19
+ drill-down back to the source atoms (Q3).
20
+
21
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
22
+ License: AGPL-3.0-or-later
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ from collections import Counter, defaultdict
30
+ from typing import Any
31
+
32
+ from superlocalmemory.core.entity_community import EntityCommunityBuilder
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ _STOPWORDS = frozenset({
37
+ "the", "a", "an", "is", "was", "were", "are", "be", "been", "being",
38
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
39
+ "should", "may", "might", "shall", "can", "to", "of", "in", "for", "on",
40
+ "with", "at", "by", "from", "as", "into", "through", "and", "but", "or",
41
+ "not", "no", "this", "that", "these", "those", "it", "its", "they",
42
+ "them", "their", "he", "she", "his", "her", "we", "our", "you", "your",
43
+ "i", "my", "me", "his", "was", "who", "what", "when", "where", "how",
44
+ })
45
+
46
+
47
+ class CommunitySummaryBuilder:
48
+ """Generate + persist one summary per entity community (background)."""
49
+
50
+ def __init__(
51
+ self,
52
+ db: Any,
53
+ summarizer: Any = None,
54
+ max_communities: int = 50,
55
+ min_facts: int = 2,
56
+ max_facts_per_community: int = 30,
57
+ max_keywords: int = 8,
58
+ summary_max_chars: int = 512,
59
+ ) -> None:
60
+ self._db = db
61
+ self._summarizer = summarizer
62
+ self._max_communities = max(1, int(max_communities))
63
+ self._min_facts = max(1, int(min_facts))
64
+ self._max_facts = max(1, int(max_facts_per_community))
65
+ self._max_keywords = max(1, int(max_keywords))
66
+ self._summary_max_chars = max(64, int(summary_max_chars))
67
+
68
+ # ------------------------------------------------------------------
69
+ # Generation
70
+ # ------------------------------------------------------------------
71
+
72
+ def compute_and_store(self, profile_id: str) -> dict[str, int]:
73
+ communities = EntityCommunityBuilder(self._db).get_communities(profile_id)
74
+ try:
75
+ self._db.execute(
76
+ "DELETE FROM community_summaries WHERE profile_id = ?",
77
+ (profile_id,),
78
+ )
79
+ except Exception as exc:
80
+ logger.debug("community_summaries clear failed: %s", exc)
81
+ if not communities:
82
+ return {"summaries_written": 0, "communities": 0}
83
+
84
+ entity_to_cid: dict[str, int] = {
85
+ e: cid for cid, ents in communities.items() for e in ents
86
+ }
87
+ cid_facts = self._gather_facts(profile_id, entity_to_cid)
88
+ name_map = self._entity_names(profile_id, communities)
89
+
90
+ written = 0
91
+ ordered = sorted(
92
+ cid_facts.items(), key=lambda kv: len(kv[1]), reverse=True,
93
+ )
94
+ for cid, facts in ordered:
95
+ if written >= self._max_communities:
96
+ break
97
+ seen: set[str] = set()
98
+ vf: list[tuple[str, str]] = []
99
+ for fid, content in facts:
100
+ if fid in seen:
101
+ continue
102
+ seen.add(fid)
103
+ vf.append((fid, content))
104
+ if len(vf) < self._min_facts:
105
+ continue
106
+
107
+ fact_ids = [fid for fid, _ in vf]
108
+ contents = [c for _, c in vf][: self._max_facts]
109
+ entity_ids = list(communities.get(cid, []))
110
+ entity_names = [name_map.get(e, e) for e in entity_ids]
111
+ keywords = self._keywords(contents)
112
+ summary = self._summary(contents, entity_names, keywords)
113
+
114
+ try:
115
+ self._db.execute(
116
+ "INSERT OR REPLACE INTO community_summaries "
117
+ "(profile_id, community_id, summary, keywords, "
118
+ " entity_ids_json, fact_ids_json, fact_count, computed_at) "
119
+ "VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))",
120
+ (
121
+ profile_id, cid, summary, keywords,
122
+ json.dumps(entity_ids), json.dumps(fact_ids),
123
+ len(fact_ids),
124
+ ),
125
+ )
126
+ written += 1
127
+ except Exception as exc:
128
+ logger.debug("community_summaries write failed (%s): %s", cid, exc)
129
+
130
+ return {"summaries_written": written, "communities": len(communities)}
131
+
132
+ # ------------------------------------------------------------------
133
+ # Read API
134
+ # ------------------------------------------------------------------
135
+
136
+ def get_summaries(self, profile_id: str) -> list[dict]:
137
+ try:
138
+ rows = self._db.execute(
139
+ "SELECT * FROM community_summaries WHERE profile_id = ? "
140
+ "ORDER BY fact_count DESC",
141
+ (profile_id,),
142
+ )
143
+ except Exception as exc:
144
+ logger.debug("get_summaries failed: %s", exc)
145
+ return []
146
+ return [dict(r) for r in rows]
147
+
148
+ def get_summary(self, profile_id: str, community_id: int) -> dict | None:
149
+ try:
150
+ rows = self._db.execute(
151
+ "SELECT * FROM community_summaries "
152
+ "WHERE profile_id = ? AND community_id = ?",
153
+ (profile_id, int(community_id)),
154
+ )
155
+ except Exception as exc:
156
+ logger.debug("get_summary failed: %s", exc)
157
+ return None
158
+ return dict(rows[0]) if rows else None
159
+
160
+ # ------------------------------------------------------------------
161
+ # Internal
162
+ # ------------------------------------------------------------------
163
+
164
+ def _gather_facts(
165
+ self, profile_id: str, entity_to_cid: dict[str, int],
166
+ ) -> dict[int, list[tuple[str, str]]]:
167
+ """One scan → {community_id -> [(fact_id, content)]}, superseded dropped."""
168
+ try:
169
+ rows = self._db.execute(
170
+ "SELECT fact_id, canonical_entities_json, content "
171
+ "FROM atomic_facts WHERE profile_id = ?",
172
+ (profile_id,),
173
+ )
174
+ except Exception as exc:
175
+ logger.debug("community fact scan failed: %s", exc)
176
+ return {}
177
+
178
+ cid_facts: dict[int, list[tuple[str, str]]] = defaultdict(list)
179
+ for row in rows:
180
+ d = dict(row)
181
+ raw = d.get("canonical_entities_json")
182
+ if not raw:
183
+ continue
184
+ try:
185
+ ents = json.loads(raw)
186
+ except (ValueError, TypeError):
187
+ continue
188
+ if not isinstance(ents, list):
189
+ continue
190
+ cids = {
191
+ entity_to_cid[str(e).strip()]
192
+ for e in ents
193
+ if str(e).strip() in entity_to_cid
194
+ }
195
+ if not cids:
196
+ continue
197
+ fid = str(d["fact_id"])
198
+ content = d.get("content") or ""
199
+ for cid in cids:
200
+ cid_facts[cid].append((fid, content))
201
+
202
+ all_fids = list({fid for lst in cid_facts.values() for fid, _ in lst})
203
+ invalid: set[str] = set()
204
+ if all_fids:
205
+ try:
206
+ invalid = self._db.get_invalidated_fact_ids(all_fids, profile_id)
207
+ except Exception as exc:
208
+ logger.debug("invalidated-fact lookup failed: %s", exc)
209
+ if invalid:
210
+ cid_facts = {
211
+ cid: [(fid, c) for fid, c in lst if fid not in invalid]
212
+ for cid, lst in cid_facts.items()
213
+ }
214
+ return cid_facts
215
+
216
+ def _entity_names(
217
+ self, profile_id: str, communities: dict[int, list[str]],
218
+ ) -> dict[str, str]:
219
+ all_eids = list({e for ents in communities.values() for e in ents})
220
+ name_map: dict[str, str] = {}
221
+ chunk = 900
222
+ for start in range(0, len(all_eids), chunk):
223
+ batch = all_eids[start:start + chunk]
224
+ ph = ",".join("?" for _ in batch)
225
+ try:
226
+ rows = self._db.execute(
227
+ "SELECT entity_id, canonical_name FROM canonical_entities "
228
+ f"WHERE profile_id = ? AND entity_id IN ({ph})",
229
+ (profile_id, *batch),
230
+ )
231
+ except Exception as exc:
232
+ logger.debug("entity-name lookup failed: %s", exc)
233
+ continue
234
+ for r in rows:
235
+ d = dict(r)
236
+ name_map[str(d["entity_id"])] = str(d.get("canonical_name") or "")
237
+ return name_map
238
+
239
+ def _keywords(self, contents: list[str]) -> str:
240
+ tokens: list[str] = []
241
+ for text in contents:
242
+ for word in text.lower().split():
243
+ w = word.strip(".,;:!?\"'()[]{}")
244
+ if len(w) > 2 and w not in _STOPWORDS:
245
+ tokens.append(w)
246
+ top = [w for w, _ in Counter(tokens).most_common(self._max_keywords)]
247
+ return ", ".join(top)
248
+
249
+ def _summary(
250
+ self, contents: list[str], entity_names: list[str], keywords: str,
251
+ ) -> str:
252
+ if self._summarizer is not None:
253
+ try:
254
+ text = self._summarizer.summarize_cluster(
255
+ [{"content": c} for c in contents],
256
+ )
257
+ if text and text.strip():
258
+ return text.strip()[: self._summary_max_chars]
259
+ except Exception as exc:
260
+ logger.debug("community summarizer failed (fail-open): %s", exc)
261
+ # Mode A keyword-dense fallback.
262
+ names = [n for n in entity_names if n]
263
+ topic = ", ".join(names[:6]) if names else "related memories"
264
+ base = f"Topics: {topic}."
265
+ if keywords:
266
+ base += f" Key terms: {keywords}."
267
+ return base[: self._summary_max_chars]
@@ -236,6 +236,11 @@ class EncodingConfig:
236
236
  # Entropy gate
237
237
  entropy_threshold: float = 0.95
238
238
 
239
+ # Entity reflexion (Wave Q1) — Mode B/C self-review of extracted entities.
240
+ # Fail-open; one extra bounded LLM call per chunk. Mode A is unaffected.
241
+ enable_entity_reflexion: bool = True
242
+ reflexion_max_facts: int = 8
243
+
239
244
 
240
245
  # ---------------------------------------------------------------------------
241
246
  # Retrieval Config
@@ -297,6 +302,11 @@ class RetrievalConfig:
297
302
  recall_per_fact_max_chars: int = 2400 # ~600 tokens; head 70% + tail 30%
298
303
  recall_total_max_chars: int = 12000 # ~3K tokens; stubs beyond this
299
304
 
305
+ # Wave Q2b: attach a precomputed community summary as thematic context when
306
+ # the top results cluster in one community. Read-only lookup, gated, and
307
+ # fail-open — never a per-query LLM call. Kill-switch for tuning.
308
+ enable_community_context: bool = True
309
+
300
310
 
301
311
  # ---------------------------------------------------------------------------
302
312
  # Math Config
@@ -659,6 +669,14 @@ class TemporalValidatorConfig:
659
669
  # Sheaf contradiction threshold
660
670
  contradiction_threshold: float = 0.45 # Mode A threshold (768d)
661
671
 
672
+ # P5-INT-01: superseded facts are DEMOTED in recall, not hidden. A fact
673
+ # marked system_expired_at keeps its channel evidence but its per-channel
674
+ # score is multiplied by this factor, so current facts rank above it while
675
+ # nothing valid silently vanishes (Mem0-2026 non-destructive design;
676
+ # retrieval-time recency resolves conflicts). 0.0 restores the old hide
677
+ # behaviour; 1.0 disables demotion.
678
+ superseded_demotion_factor: float = 0.25
679
+
662
680
  # LLM pre-filter threshold (lower to catch more candidates)
663
681
  llm_prefilter_threshold: float = 0.30
664
682
 
@@ -677,18 +695,31 @@ class EvolutionConfig:
677
695
  """Configuration for Skill Evolution Engine (v3.4.10).
678
696
 
679
697
  OFF by default — opt in via `slm setup` (interactive) or
680
- `slm config set evolution.enabled true` (CLI).
698
+ `slm config set evolution.enabled true` (CLI). Enabling makes
699
+ background LLM calls; the enable flow surfaces a cost advisory.
681
700
 
682
701
  Backend auto-detection priority:
683
702
  1. `claude` CLI available → spawn `claude --model haiku` (ECC pattern, free)
684
703
  2. Ollama running → use Ollama (free, local)
685
704
  3. API key set → use Anthropic/OpenAI API (paid)
686
705
  4. Nothing → dashboard-only (show candidates, manual evolution)
706
+
707
+ Model selection (v3.7.9): each pipeline step is configurable and
708
+ defaults to the lowest-cost capable model for the active backend
709
+ (Claude→haiku, Ollama→local). Leave a field empty ("") for "auto".
710
+ The blind verifier is kept on a *different* model from the generator
711
+ so it can't grade its own homework; see
712
+ ``evolution.model_selection.resolve_evolution_models``.
687
713
  """
688
714
 
689
715
  enabled: bool = False # OFF by default, opt-in
690
716
  backend: str = "auto" # auto, claude, ollama, anthropic, openai
691
717
  max_evolutions_per_cycle: int = 3 # Budget cap per consolidation
718
+ # Empty string == "auto" (resolve cheapest capable model at runtime).
719
+ # Accepts short aliases ("haiku"/"sonnet") or allow-listed model ids.
720
+ mutation_model: str = "" # generator (quality-sensitive)
721
+ verify_model: str = "" # blind verifier (must differ from generator)
722
+ confirm_model: str = "" # cheap yes/no gate
692
723
 
693
724
 
694
725
  @dataclass(frozen=True)
@@ -743,6 +774,152 @@ class AutoInvokeConfig:
743
774
  relevance_threshold: float = 0.3 # Legacy compat with AutoRecall
744
775
 
745
776
 
777
+ # ---------------------------------------------------------------------------
778
+ # Deployment Config (v3.8.0)
779
+ # ---------------------------------------------------------------------------
780
+
781
+ _VALID_DEPLOYMENT_MODES = ("personal", "enterprise")
782
+
783
+
784
+ @dataclass(frozen=True)
785
+ class DeploymentConfig:
786
+ """Deployment mode configuration.
787
+
788
+ Personal (default, safe defaults):
789
+ Single-user install. No login required, no PII redaction, no
790
+ retention scheduler. Behaviour is identical to pre-3.8.0 installs.
791
+
792
+ Enterprise:
793
+ Multi-user / team / company install. Login required, PII redacted,
794
+ retention scheduler active, audit enabled.
795
+
796
+ SAFE DEFAULTS = Personal — an existing config.toml with no [deployment]
797
+ section continues to behave exactly as before. Adding a [deployment]
798
+ section is opt-in; removing it reverts to Personal automatically.
799
+ """
800
+
801
+ mode: str = "personal" # "personal" | "enterprise"
802
+ require_login: bool = False
803
+ pii_redaction: bool = False
804
+ retention_enabled: bool = False
805
+ audit: bool = True
806
+
807
+ def __post_init__(self) -> None:
808
+ if self.mode not in _VALID_DEPLOYMENT_MODES:
809
+ raise ValueError(
810
+ f"DeploymentConfig.mode must be one of {_VALID_DEPLOYMENT_MODES!r}, "
811
+ f"got {self.mode!r}"
812
+ )
813
+
814
+ @property
815
+ def is_personal(self) -> bool:
816
+ """True when operating in personal (single-user) mode."""
817
+ return self.mode == "personal"
818
+
819
+ @property
820
+ def is_enterprise(self) -> bool:
821
+ """True when operating in enterprise (team/company) mode."""
822
+ return self.mode == "enterprise"
823
+
824
+ def as_dict(self) -> dict:
825
+ return {
826
+ "mode": self.mode,
827
+ "require_login": self.require_login,
828
+ "pii_redaction": self.pii_redaction,
829
+ "retention_enabled": self.retention_enabled,
830
+ "audit": self.audit,
831
+ }
832
+
833
+
834
+ #: Canonical Personal preset — all permissive defaults.
835
+ DEPLOYMENT_PERSONAL = DeploymentConfig(
836
+ mode="personal",
837
+ require_login=False,
838
+ pii_redaction=False,
839
+ retention_enabled=False,
840
+ audit=True,
841
+ )
842
+
843
+ #: Canonical Enterprise preset — all enforcement defaults.
844
+ DEPLOYMENT_ENTERPRISE = DeploymentConfig(
845
+ mode="enterprise",
846
+ require_login=True,
847
+ pii_redaction=True,
848
+ retention_enabled=True,
849
+ audit=True,
850
+ )
851
+
852
+
853
+ def load_deployment_config(
854
+ config_toml_path: Path | None = None,
855
+ ) -> DeploymentConfig:
856
+ """Parse [deployment] from config.toml; return Personal defaults if absent.
857
+
858
+ config.toml is the installer-written performance config (separate from the
859
+ daemon's config.json managed by SLMConfig). This function reads ONLY the
860
+ [deployment] section. All other sections are ignored.
861
+
862
+ Args:
863
+ config_toml_path: Explicit path to config.toml. When None, resolved to
864
+ ``~/.superlocalmemory/config.toml`` via the canonical data root.
865
+
866
+ Returns:
867
+ DeploymentConfig — Personal preset when the file is absent, the section
868
+ is missing, or any parse error occurs (fail-open, non-destructive).
869
+ """
870
+ if config_toml_path is None:
871
+ try:
872
+ config_toml_path = _runtime_base_dir() / "config.toml"
873
+ except Exception:
874
+ return DEPLOYMENT_PERSONAL
875
+
876
+ if not config_toml_path.exists():
877
+ return DEPLOYMENT_PERSONAL
878
+
879
+ try:
880
+ import tomllib as _tomllib
881
+ raw = config_toml_path.read_text(encoding="utf-8")
882
+ data = _tomllib.loads(raw)
883
+ except Exception as exc:
884
+ logger.warning(
885
+ "load_deployment_config: failed to parse %s: %s", config_toml_path, exc
886
+ )
887
+ return DEPLOYMENT_PERSONAL
888
+
889
+ dep = data.get("deployment", {})
890
+ if not dep:
891
+ # No [deployment] section — personal defaults, no behaviour change.
892
+ return DEPLOYMENT_PERSONAL
893
+
894
+ raw_mode = str(dep.get("mode", "personal")).lower()
895
+ if raw_mode not in _VALID_DEPLOYMENT_MODES:
896
+ logger.warning(
897
+ "load_deployment_config: unknown mode %r in %s — defaulting to personal",
898
+ raw_mode, config_toml_path,
899
+ )
900
+ raw_mode = "personal"
901
+
902
+ # Use the preset for the mode as the base so omitted keys inherit
903
+ # sensible values (enterprise → require_login=True etc.).
904
+ base = DEPLOYMENT_ENTERPRISE if raw_mode == "enterprise" else DEPLOYMENT_PERSONAL
905
+
906
+ try:
907
+ return DeploymentConfig(
908
+ mode=raw_mode,
909
+ require_login=bool(dep.get("require_login", base.require_login)),
910
+ pii_redaction=bool(dep.get("pii_redaction", base.pii_redaction)),
911
+ retention_enabled=bool(dep.get("retention_enabled", base.retention_enabled)),
912
+ audit=bool(dep.get("audit", base.audit)),
913
+ )
914
+ except (ValueError, TypeError) as exc:
915
+ logger.warning(
916
+ "load_deployment_config: invalid values in [deployment] in %s: %s — "
917
+ "falling back to personal",
918
+ config_toml_path, exc,
919
+ )
920
+ return DEPLOYMENT_PERSONAL
921
+
922
+
746
923
  # ---------------------------------------------------------------------------
747
924
  # Health Config (v3.6.9 BUG-A)
748
925
  # ---------------------------------------------------------------------------
@@ -849,6 +1026,35 @@ class SLMConfig:
849
1026
  path = config_path or (_runtime_base / "config.json")
850
1027
  if not path.exists():
851
1028
  return cls.for_mode(Mode.A, base_dir=_runtime_base)
1029
+
1030
+ # DASH-V4 (3.7.9): the ``current_mode`` file is the single source of
1031
+ # truth for the ACTIVE mode. If config.json drifted from it (a writer
1032
+ # bypassed switch_mode and reset the active config), load the
1033
+ # authoritative per-mode file so daemon/CLI/dashboard all run the user's
1034
+ # chosen mode — with its settings — instead of a stale one. Scoped to
1035
+ # config.json so explicit per-mode loads (switch_mode) are untouched.
1036
+ # Fail-open: any error falls through to the normal load below.
1037
+ if path.name == "config.json":
1038
+ try:
1039
+ import json as _json
1040
+ _disk_mode = str(
1041
+ _json.loads(path.read_text()).get("mode", "")
1042
+ ).lower()
1043
+ _active_mode = cls.read_current_mode(path.parent)
1044
+ if _disk_mode and _active_mode and _disk_mode != _active_mode:
1045
+ _mode_path = cls._mode_config_path(
1046
+ path.parent, Mode(_active_mode)
1047
+ )
1048
+ if _mode_path.exists():
1049
+ logger.warning(
1050
+ "config.json mode=%s disagrees with current_mode=%s;"
1051
+ " loading authoritative %s",
1052
+ _disk_mode, _active_mode, _mode_path.name,
1053
+ )
1054
+ return cls.load(_mode_path)
1055
+ except Exception:
1056
+ pass # fail-open — fall through to the normal load below
1057
+
852
1058
  import json
853
1059
  try:
854
1060
  data = json.loads(path.read_text())
@@ -1070,6 +1276,9 @@ class SLMConfig:
1070
1276
  "enabled": self.evolution.enabled,
1071
1277
  "backend": self.evolution.backend,
1072
1278
  "max_evolutions_per_cycle": self.evolution.max_evolutions_per_cycle,
1279
+ "mutation_model": self.evolution.mutation_model,
1280
+ "verify_model": self.evolution.verify_model,
1281
+ "confirm_model": self.evolution.confirm_model,
1073
1282
  }
1074
1283
 
1075
1284
  # V3.4.65: Persist injection config
@@ -1264,6 +1473,7 @@ class SLMConfig:
1264
1473
  _c_emb = EmbeddingConfig(
1265
1474
  model_name=embedding_model_name or "text-embedding-3-large",
1266
1475
  dimension=embedding_dimension or 3072,
1476
+ provider=_c_emb_provider,
1267
1477
  api_endpoint=embedding_endpoint,
1268
1478
  api_key=embedding_key,
1269
1479
  deployment_name=embedding_deployment,
@@ -1275,8 +1485,11 @@ class SLMConfig:
1275
1485
  # 768-dim Ollama embedder, which made ingestion fail at vector
1276
1486
  # materialization. Cloud embeddings remain an explicit opt-in.
1277
1487
  _c_emb = EmbeddingConfig(
1278
- model_name="nomic-ai/nomic-embed-text-v1.5",
1279
- dimension=768,
1488
+ # Honour an on-disk embedding model when one was configured (the
1489
+ # load() path passes it through); default to the local nomic
1490
+ # model so Mode C never silently requires a paid cloud embedder.
1491
+ model_name=embedding_model_name or "nomic-ai/nomic-embed-text-v1.5",
1492
+ dimension=embedding_dimension or 768,
1280
1493
  )
1281
1494
  return cls(
1282
1495
  mode=mode,