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
@@ -0,0 +1,200 @@
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
+ """Reflexion self-review over LLM-extracted entities (Wave Q1).
6
+
7
+ After primary LLM extraction (Mode B/C only), one bounded self-review pass
8
+ audits the extracted entities against the source text and:
9
+
10
+ - drops hallucinated entities (not supported by the source), and
11
+ - adds clearly-missed entities (whose exact text appears in the source).
12
+
13
+ This closes the two distinct extraction failure modes the market leaders
14
+ separate: coverage (missed) and grounding (hallucinated). Coverage is the
15
+ "gleaning" idea (re-present what was extracted, ask for misses); grounding is
16
+ the "reflexion" idea (verify each extracted entity is real).
17
+
18
+ Design guarantees:
19
+ - Mode A never reaches here — the caller gates on Mode B/C + LLM available.
20
+ - Fail-open: any error, unavailable LLM, or unparseable output returns the
21
+ input facts unchanged. This module NEVER raises.
22
+ - Immutable: corrected facts are new objects; inputs are not mutated.
23
+ - Bounded: one LLM call, capped facts reviewed, capped entities per fact.
24
+
25
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
26
+ License: AGPL-3.0-or-later
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import logging
33
+ import re
34
+ from dataclasses import replace
35
+ from typing import Any
36
+
37
+ from superlocalmemory.storage.models import AtomicFact
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ _MAX_ENTITIES_PER_FACT = 12
42
+ _MIN_ENTITY_LEN = 2
43
+
44
+ _SYSTEM = (
45
+ "You are a precise information-extraction auditor. You verify that "
46
+ "entities extracted from text are actually supported by that text, and "
47
+ "you catch clearly-named entities that were missed. You never invent "
48
+ "entities. You reply with JSON only."
49
+ )
50
+
51
+
52
+ class EntityReflexion:
53
+ """Bounded, fail-open reflexion pass over extracted entities."""
54
+
55
+ def __init__(
56
+ self,
57
+ llm: Any,
58
+ max_facts: int = 8,
59
+ max_tokens: int = 512,
60
+ ) -> None:
61
+ self._llm = llm
62
+ self._max_facts = max(1, int(max_facts))
63
+ self._max_tokens = max(64, int(max_tokens))
64
+
65
+ # ------------------------------------------------------------------
66
+ # Public API
67
+ # ------------------------------------------------------------------
68
+
69
+ def refine(
70
+ self, source_text: str, facts: list[AtomicFact],
71
+ ) -> list[AtomicFact]:
72
+ """Return facts with entity lists corrected, or unchanged on failure."""
73
+ if not self._llm or not facts or not source_text or not source_text.strip():
74
+ return facts
75
+ try:
76
+ is_avail = getattr(self._llm, "is_available", None)
77
+ if callable(is_avail) and not is_avail():
78
+ return facts
79
+ except Exception:
80
+ return facts
81
+
82
+ subject = facts[: self._max_facts]
83
+ try:
84
+ raw = self._invoke(source_text, subject)
85
+ except Exception as exc:
86
+ logger.debug("EntityReflexion LLM call failed: %s", exc)
87
+ return facts
88
+
89
+ corrections = self._parse(raw)
90
+ if not corrections:
91
+ return facts
92
+ try:
93
+ return self._apply(facts, corrections, source_text)
94
+ except Exception as exc:
95
+ logger.debug("EntityReflexion apply failed: %s", exc)
96
+ return facts
97
+
98
+ # ------------------------------------------------------------------
99
+ # Internal
100
+ # ------------------------------------------------------------------
101
+
102
+ def _invoke(self, source_text: str, subject: list[AtomicFact]) -> str:
103
+ lines = []
104
+ for i, f in enumerate(subject):
105
+ lines.append(f'{i}: "{f.content}" entities={list(f.entities)}')
106
+ facts_block = "\n".join(lines)
107
+ prompt = (
108
+ "Audit the extracted entities for each fact against the SOURCE "
109
+ "text below.\n"
110
+ "For each fact, report:\n"
111
+ "- drop: entities in the fact's list that are NOT supported by the "
112
+ "source (hallucinated).\n"
113
+ "- add: named entities clearly present in the source but missing "
114
+ "from the fact's list. Only add entities whose exact text appears "
115
+ "in the source.\n\n"
116
+ f"--- SOURCE ---\n{source_text}\n--- END ---\n\n"
117
+ f"--- FACTS ---\n{facts_block}\n--- END ---\n\n"
118
+ 'Respond with ONLY a JSON array: '
119
+ '[{"index": <int>, "drop": [..], "add": [..]}]. '
120
+ "Use [] for an empty list. Omit facts that need no change."
121
+ )
122
+ out = self._llm.generate(
123
+ prompt=prompt,
124
+ system=_SYSTEM,
125
+ temperature=0.0,
126
+ max_tokens=self._max_tokens,
127
+ )
128
+ return out if isinstance(out, str) else str(out or "")
129
+
130
+ @staticmethod
131
+ def _parse(raw: str) -> list[dict]:
132
+ if not raw or not raw.strip():
133
+ return []
134
+ match = re.search(r"\[.*\]", raw, re.DOTALL)
135
+ if not match:
136
+ return []
137
+ try:
138
+ data = json.loads(match.group())
139
+ except (json.JSONDecodeError, ValueError):
140
+ return []
141
+ if not isinstance(data, list):
142
+ return []
143
+ return [d for d in data if isinstance(d, dict)]
144
+
145
+ def _apply(
146
+ self,
147
+ facts: list[AtomicFact],
148
+ corrections: list[dict],
149
+ source_text: str,
150
+ ) -> list[AtomicFact]:
151
+ by_index: dict[int, dict] = {}
152
+ for c in corrections:
153
+ idx = c.get("index")
154
+ if isinstance(idx, int) and 0 <= idx < self._max_facts:
155
+ by_index[idx] = c
156
+
157
+ src_low = source_text.lower()
158
+ out: list[AtomicFact] = []
159
+ for i, fact in enumerate(facts):
160
+ c = by_index.get(i)
161
+ if c is None:
162
+ out.append(fact)
163
+ continue
164
+
165
+ ents = list(fact.entities)
166
+ drop = {
167
+ str(d).strip().lower()
168
+ for d in _as_list(c.get("drop"))
169
+ if str(d).strip()
170
+ }
171
+ if drop:
172
+ ents = [e for e in ents if e.strip().lower() not in drop]
173
+
174
+ seen = {e.strip().lower() for e in ents}
175
+ for a in _as_list(c.get("add")):
176
+ cand = str(a).strip()
177
+ low = cand.lower()
178
+ if len(cand) < _MIN_ENTITY_LEN or low in seen:
179
+ continue
180
+ # Grounding guard: only add entities literally present in source.
181
+ if low not in src_low:
182
+ continue
183
+ ents.append(cand)
184
+ seen.add(low)
185
+ if len(ents) >= _MAX_ENTITIES_PER_FACT:
186
+ break
187
+
188
+ if ents != list(fact.entities):
189
+ out.append(replace(fact, entities=ents))
190
+ else:
191
+ out.append(fact)
192
+ return out
193
+
194
+
195
+ def _as_list(value: Any) -> list:
196
+ if isinstance(value, list):
197
+ return value
198
+ if value is None:
199
+ return []
200
+ return [value]
@@ -283,22 +283,22 @@ class EntityResolver:
283
283
  entity = self._db.get_entity_by_name(name, profile_id)
284
284
  if entity is not None:
285
285
  resolution[raw] = entity.entity_id
286
- self._touch_last_seen(entity.entity_id)
286
+ self._touch_last_seen(entity.entity_id, profile_id)
287
287
  continue
288
288
 
289
289
  # Tier b: alias match (case-insensitive, indexed)
290
290
  entity_id = self._alias_lookup(name, profile_id)
291
291
  if entity_id is not None:
292
292
  resolution[raw] = entity_id
293
- self._touch_last_seen(entity_id)
293
+ self._touch_last_seen(entity_id, profile_id)
294
294
  continue
295
295
 
296
296
  # Tier c: fuzzy match via Jaro-Winkler
297
297
  match_id, score = self._fuzzy_match(name, profile_id)
298
298
  if match_id is not None and score >= JARO_WINKLER_AUTO_MERGE:
299
299
  resolution[raw] = match_id
300
- self._persist_alias(match_id, name, score, "jaro_winkler")
301
- self._touch_last_seen(match_id)
300
+ self._persist_alias(match_id, name, score, "jaro_winkler", profile_id)
301
+ self._touch_last_seen(match_id, profile_id)
302
302
  continue
303
303
 
304
304
  # Candidate zone (0.70–0.85): queue for LLM in Mode B/C
@@ -384,7 +384,7 @@ class EntityResolver:
384
384
  and removes the merged entity record.
385
385
  """
386
386
  # Move aliases from merge -> keep
387
- aliases = self._db.get_aliases_for_entity(entity_id_merge)
387
+ aliases = self._db.get_aliases_for_entity(entity_id_merge, profile_id)
388
388
  for alias in aliases:
389
389
  new_alias = EntityAlias(
390
390
  alias_id=_new_id(),
@@ -393,7 +393,7 @@ class EntityResolver:
393
393
  confidence=alias.confidence,
394
394
  source=f"merge_from:{entity_id_merge}",
395
395
  )
396
- self._db.store_alias(new_alias)
396
+ self._db.store_alias(new_alias, profile_id)
397
397
 
398
398
  # Also add the merged entity's canonical name as an alias of keep
399
399
  merged = self._db.get_entity_by_name("", "") # placeholder
@@ -403,7 +403,7 @@ class EntityResolver:
403
403
  )
404
404
  if rows:
405
405
  merged_name = str(dict(rows[0])["canonical_name"])
406
- self._persist_alias(entity_id_keep, merged_name, 1.0, "merge")
406
+ self._persist_alias(entity_id_keep, merged_name, 1.0, "merge", profile_id)
407
407
 
408
408
  # Update atomic_facts: replace entity_id_merge with entity_id_keep
409
409
  # in canonical_entities_json column
@@ -435,14 +435,14 @@ class EntityResolver:
435
435
  except (json.JSONDecodeError, TypeError):
436
436
  continue
437
437
 
438
- # Delete the merged entity
438
+ # Delete the merged entity (tenant-scoped)
439
439
  self._db.execute(
440
- "DELETE FROM entity_aliases WHERE entity_id = ?",
441
- (entity_id_merge,),
440
+ "DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
441
+ (entity_id_merge, profile_id),
442
442
  )
443
443
  self._db.execute(
444
- "DELETE FROM canonical_entities WHERE entity_id = ?",
445
- (entity_id_merge,),
444
+ "DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
445
+ (entity_id_merge, profile_id),
446
446
  )
447
447
  logger.info(
448
448
  "Merged entity %s into %s (profile=%s)",
@@ -526,7 +526,7 @@ class EntityResolver:
526
526
  self._db.store_entity(entity)
527
527
 
528
528
  # Store name as its own alias for uniform lookup
529
- self._persist_alias(entity.entity_id, name, 1.0, "canonical")
529
+ self._persist_alias(entity.entity_id, name, 1.0, "canonical", profile_id)
530
530
 
531
531
  logger.debug(
532
532
  "Created entity '%s' [%s] (type=%s, profile=%s)",
@@ -540,13 +540,16 @@ class EntityResolver:
540
540
  alias_text: str,
541
541
  confidence: float,
542
542
  source: str,
543
+ profile_id: str,
543
544
  ) -> None:
544
- """Store an alias, skipping duplicates."""
545
- # Check if alias already exists for this entity
545
+ """Store an alias under a profile, skipping duplicates within it."""
546
+ # Check if alias already exists for this entity IN THIS PROFILE. The
547
+ # dedup must be profile-scoped or one profile's alias suppresses
548
+ # another profile's identical alias for a same-id entity.
546
549
  existing = self._db.execute(
547
550
  "SELECT alias_id FROM entity_aliases "
548
- "WHERE entity_id = ? AND LOWER(alias) = LOWER(?)",
549
- (entity_id, alias_text),
551
+ "WHERE entity_id = ? AND profile_id = ? AND LOWER(alias) = LOWER(?)",
552
+ (entity_id, profile_id, alias_text),
550
553
  )
551
554
  if existing:
552
555
  return
@@ -557,13 +560,20 @@ class EntityResolver:
557
560
  confidence=confidence,
558
561
  source=source,
559
562
  )
560
- self._db.store_alias(alias)
563
+ self._db.store_alias(alias, profile_id)
561
564
 
562
- def _touch_last_seen(self, entity_id: str) -> None:
563
- """Update last_seen timestamp on a canonical entity."""
565
+ def _touch_last_seen(self, entity_id: str, profile_id: str = "default") -> None:
566
+ """Update last_seen timestamp on a canonical entity scoped to profile.
567
+
568
+ L-01 fix: the original query had no profile_id guard. Since entity_ids are
569
+ UUIDs today the blast radius is theoretical, but the guard is required for
570
+ defense-in-depth against future import/sharing features that could introduce
571
+ UUID collisions across profiles.
572
+ """
564
573
  self._db.execute(
565
- "UPDATE canonical_entities SET last_seen = ? WHERE entity_id = ?",
566
- (_now(), entity_id),
574
+ "UPDATE canonical_entities SET last_seen = ? "
575
+ "WHERE entity_id = ? AND profile_id = ?",
576
+ (_now(), entity_id, profile_id),
567
577
  )
568
578
 
569
579
  # -- Internal: LLM disambiguation (Mode B/C) ---------------------------
@@ -621,9 +631,9 @@ class EntityResolver:
621
631
  entity_id = known[name_str]
622
632
  resolved[mention_str] = entity_id
623
633
  self._persist_alias(
624
- entity_id, mention_str, 0.9, "llm",
634
+ entity_id, mention_str, 0.9, "llm", profile_id,
625
635
  )
626
- self._touch_last_seen(entity_id)
636
+ self._touch_last_seen(entity_id, profile_id)
627
637
  # If LLM says it's itself, leave for caller to create
628
638
  return resolved
629
639
 
@@ -685,11 +685,36 @@ class FactExtractor:
685
685
  temperature=0.0,
686
686
  max_tokens=1024,
687
687
  )
688
- return self._parse_llm_response(raw, session_id, session_date)
688
+ facts = self._parse_llm_response(raw, session_id, session_date)
689
+ return self._reflexion_refine(conversation_text, facts)
689
690
  except Exception as exc:
690
691
  logger.warning("LLM fact extraction failed: %s", exc)
691
692
  return []
692
693
 
694
+ def _reflexion_refine(
695
+ self, source_text: str, facts: list[AtomicFact],
696
+ ) -> list[AtomicFact]:
697
+ """Wave Q1: bounded, fail-open entity self-review (Mode B/C only).
698
+
699
+ Reached only from the LLM extraction path, so Mode A never runs this.
700
+ Any failure returns the facts unchanged.
701
+ """
702
+ if not facts or not getattr(
703
+ self._config, "enable_entity_reflexion", True,
704
+ ):
705
+ return facts
706
+ try:
707
+ from superlocalmemory.encoding.entity_reflexion import EntityReflexion
708
+
709
+ reflexion = EntityReflexion(
710
+ self._llm,
711
+ max_facts=getattr(self._config, "reflexion_max_facts", 8),
712
+ )
713
+ return reflexion.refine(source_text, facts)
714
+ except Exception as exc:
715
+ logger.debug("Entity reflexion skipped: %s", exc)
716
+ return facts
717
+
693
718
  def _parse_llm_response(
694
719
  self,
695
720
  raw: str,
@@ -96,6 +96,18 @@ class TemporalValidator:
96
96
  severity = contradiction["severity"]
97
97
  reason = contradiction["description"]
98
98
 
99
+ # Temporal-anchor guard: if the new fact and the old fact carry
100
+ # distinct explicit event anchors, they describe the same subject at
101
+ # different times (a valid timeline — "lived in Delhi (2020)" vs
102
+ # "lives in Mumbai (2024)"), NOT a contradiction. Do not supersede;
103
+ # both stay recallable so historical queries still find them.
104
+ if self._is_historical_progression(new_fact, old_fact_id):
105
+ logger.debug(
106
+ "Temporal: %s vs %s have distinct event anchors — historical "
107
+ "progression, not superseding", new_fact.fact_id, old_fact_id,
108
+ )
109
+ continue
110
+
99
111
  # Step 1: Invalidate the old fact (bi-temporal)
100
112
  self.invalidate_fact(
101
113
  fact_id=old_fact_id,
@@ -119,6 +131,54 @@ class TemporalValidator:
119
131
  )
120
132
  return actions
121
133
 
134
+ @staticmethod
135
+ def _event_anchor(
136
+ referenced_date: str | None,
137
+ observation_date: str | None,
138
+ interval_start: str | None,
139
+ ) -> str | None:
140
+ """The most-specific explicit event time, or None when undated."""
141
+ return referenced_date or observation_date or interval_start or None
142
+
143
+ def _is_historical_progression(
144
+ self, new_fact: AtomicFact, old_fact_id: str,
145
+ ) -> bool:
146
+ """True when new_fact and the old fact have distinct explicit event
147
+ anchors (same subject, different times) — a valid timeline rather than a
148
+ contradiction. Conservative: False unless BOTH anchors are present and
149
+ resolve to different calendar days, so undated facts supersede as before.
150
+ """
151
+ new_anchor = self._event_anchor(
152
+ getattr(new_fact, "referenced_date", None),
153
+ getattr(new_fact, "observation_date", None),
154
+ getattr(new_fact, "interval_start", None),
155
+ )
156
+ if not new_anchor:
157
+ return False
158
+ try:
159
+ rows = self._db.execute(
160
+ "SELECT referenced_date, observation_date, interval_start "
161
+ "FROM atomic_facts WHERE fact_id = ?",
162
+ (old_fact_id,),
163
+ )
164
+ except Exception:
165
+ return False
166
+ if not rows:
167
+ return False
168
+ d = dict(rows[0])
169
+ old_anchor = self._event_anchor(
170
+ d.get("referenced_date"), d.get("observation_date"),
171
+ d.get("interval_start"),
172
+ )
173
+ if not old_anchor:
174
+ return False
175
+ from superlocalmemory.retrieval.time_window import parse_timestamp
176
+ nd = parse_timestamp(new_anchor)
177
+ od = parse_timestamp(old_anchor)
178
+ if nd is None or od is None:
179
+ return False
180
+ return nd.date() != od.date()
181
+
122
182
  def detect_contradiction(
123
183
  self,
124
184
  new_fact: AtomicFact,
@@ -206,7 +266,10 @@ class TemporalValidator:
206
266
  Both params are REQUIRED in the call site. Do NOT rename to is_valid().
207
267
  """
208
268
  try:
209
- tv = self._db.get_temporal_validity(fact_id)
269
+ # profile_id or None: an empty-string default must NOT become a
270
+ # `profile_id = ''` filter (which never matches → an expired fact
271
+ # would be reported valid). None falls back to the unscoped lookup.
272
+ tv = self._db.get_temporal_validity(fact_id, profile_id or None)
210
273
  if tv is None:
211
274
  return True # No temporal record = assumed valid
212
275
  return (