superlocalmemory 3.7.8 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +827 -72
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -11,8 +11,10 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
- import logging
14
+ import hashlib
15
15
  import json
16
+ import logging
17
+ import uuid
16
18
  from typing import TYPE_CHECKING, Any
17
19
 
18
20
  if TYPE_CHECKING:
@@ -21,7 +23,9 @@ if TYPE_CHECKING:
21
23
  from superlocalmemory.storage.database import DatabaseManager
22
24
 
23
25
  from superlocalmemory.storage.models import (
24
- AtomicFact, FactType, MemoryRecord,
26
+ AtomicFact,
27
+ FactType,
28
+ MemoryRecord,
25
29
  )
26
30
 
27
31
  logger = logging.getLogger(__name__)
@@ -30,6 +34,63 @@ logger = logging.getLogger(__name__)
30
34
  _INIT_LANGEVIN_RADIUS = 0.05
31
35
 
32
36
 
37
+ def _ingestion_effect_id(operation_id: str, *parts: object) -> str:
38
+ """Return a stable ID for a relational effect owned by one ingestion."""
39
+ if not operation_id:
40
+ return uuid.uuid4().hex
41
+ payload = "\0".join((operation_id, *(str(part) for part in parts)))
42
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]
43
+
44
+
45
+ def _record_fact_entity_association(
46
+ db: DatabaseManager,
47
+ *,
48
+ operation_id: str,
49
+ profile_id: str,
50
+ fact_id: str,
51
+ entity_id: str,
52
+ ) -> None:
53
+ """Apply one fact/entity count effect in O(1), exactly once."""
54
+ if not operation_id:
55
+ db.increment_entity_fact_count(entity_id, profile_id)
56
+ return
57
+ with db.transaction():
58
+ claimed = db.execute(
59
+ "INSERT INTO fact_entity_associations "
60
+ "(profile_id,fact_id,entity_id,first_operation_id,count_applied) "
61
+ "SELECT ?,?,?,?,"
62
+ "CASE WHEN fact.rowid > repair.target_fact_rowid THEN 1 ELSE 0 END "
63
+ "FROM canonical_entities AS entity "
64
+ "JOIN atomic_facts AS fact "
65
+ "ON fact.fact_id=? AND fact.profile_id=? "
66
+ "JOIN fact_entity_association_repair_state AS repair "
67
+ "ON repair.repair_key='historical-backfill' "
68
+ "WHERE entity.entity_id=? AND entity.profile_id=? "
69
+ "ON CONFLICT(profile_id,fact_id,entity_id) DO UPDATE SET "
70
+ "count_applied=excluded.count_applied,"
71
+ "first_operation_id=excluded.first_operation_id "
72
+ "WHERE fact_entity_associations.count_applied=0 "
73
+ "AND excluded.count_applied=1 "
74
+ "RETURNING count_applied",
75
+ (
76
+ profile_id,
77
+ fact_id,
78
+ entity_id,
79
+ operation_id,
80
+ fact_id,
81
+ profile_id,
82
+ entity_id,
83
+ profile_id,
84
+ ),
85
+ )
86
+ if claimed and int(claimed[0]["count_applied"]) == 1:
87
+ db.execute(
88
+ "UPDATE canonical_entities SET fact_count=fact_count+1 "
89
+ "WHERE entity_id=? AND profile_id=?",
90
+ (entity_id, profile_id),
91
+ )
92
+
93
+
33
94
  def _init_langevin_position(dim: int = 8) -> list[float]:
34
95
  """Initialize Langevin position near origin for a new fact.
35
96
 
@@ -60,7 +121,7 @@ def enrich_fact(
60
121
  temporal_parser: Any,
61
122
  ) -> AtomicFact:
62
123
  """Enrich fact with embeddings, entities, temporal, emotional data."""
63
- from superlocalmemory.encoding.emotional import tag_emotion, emotional_importance_boost
124
+ from superlocalmemory.encoding.emotional import emotional_importance_boost, tag_emotion
64
125
  from superlocalmemory.encoding.signal_inference import infer_signal
65
126
 
66
127
  embedding = embedder.embed(fact.content) if embedder else None
@@ -189,6 +250,9 @@ def run_store(
189
250
  ingestion_source_type: str = "store",
190
251
  ingestion_operation_id: str = "",
191
252
  derivation_report: dict[str, bool] | None = None,
253
+ precompleted_derivation_stages: frozenset[str] = frozenset(),
254
+ materialization_progress: dict[str, Any] | None = None,
255
+ materialization_checkpoint: Any = None,
192
256
  ) -> list[str]:
193
257
  """Store content and extract structured facts. Returns fact_ids.
194
258
 
@@ -208,21 +272,33 @@ def run_store(
208
272
  if not pre_authorized:
209
273
  hooks.run_pre("store", hook_ctx)
210
274
 
211
- if entropy_gate and not entropy_gate.should_pass(content):
275
+ # Admission gates apply to FRESH submissions only. A materialization pass
276
+ # re-runs this pipeline for content whose queryable projection was already
277
+ # committed at submit (``queryable_fact_ids`` is set). Re-applying admission
278
+ # here — the entropy near-duplicate gate especially — would discard that
279
+ # already-committed fact (return []) and wedge the operation in an endless
280
+ # materialize retry loop ("materialization produced no final facts"). The
281
+ # near-duplicate verdict is expected at materialize: the submitted
282
+ # projection itself is in the gate's window. Admission was already decided
283
+ # at submit (store_fast enforces the same gates), so skip it here.
284
+ is_materialization = bool(queryable_fact_ids)
285
+
286
+ if entropy_gate and not is_materialization and not entropy_gate.should_pass(content):
212
287
  return []
213
288
 
214
289
  # v3.5.0: store-side quality gate (H3). Reject prompt-template leakage,
215
290
  # empty placeholders, and other non-memory content BEFORE it enters the
216
291
  # DB. Uses the shared is_low_quality from core/injection so both store
217
292
  # AND injection filter by identical rules. Saves DB IO + recall pollution.
218
- try:
219
- from superlocalmemory.core.injection import is_low_quality
220
- if is_low_quality(content):
221
- logger.debug("Store rejected (low-quality content): %s...",
222
- content[:80].replace("\n", " "))
223
- return []
224
- except Exception:
225
- pass # Best-effort gate; store succeeds if import fails
293
+ if not is_materialization:
294
+ try:
295
+ from superlocalmemory.core.injection import is_low_quality
296
+ if is_low_quality(content):
297
+ logger.debug("Store rejected (low-quality content): %s...",
298
+ content[:80].replace("\n", " "))
299
+ return []
300
+ except Exception:
301
+ pass # Best-effort gate; store succeeds if import fails
226
302
 
227
303
  from superlocalmemory.encoding.temporal_parser import TemporalParser
228
304
  parser = temporal_parser or TemporalParser()
@@ -274,10 +350,19 @@ def run_store(
274
350
  )
275
351
  db.store_memory(record)
276
352
 
277
- extraction_complete = False
278
- consolidation_complete = consolidator is not None
279
- canonicalization_complete = entity_resolver is not None
280
- graph_complete = graph_builder is not None
353
+ extraction_complete = "extraction" in precompleted_derivation_stages
354
+ consolidation_complete = (
355
+ "consolidation" in precompleted_derivation_stages
356
+ or consolidator is not None
357
+ )
358
+ canonicalization_complete = (
359
+ "canonicalization" in precompleted_derivation_stages
360
+ or entity_resolver is not None
361
+ )
362
+ graph_complete = (
363
+ "graph" in precompleted_derivation_stages
364
+ or graph_builder is not None
365
+ )
281
366
  temporal_complete = True
282
367
  provenance_complete = provenance is not None
283
368
 
@@ -318,7 +403,6 @@ def run_store(
318
403
  and content.strip()
319
404
  and len(content.strip()) >= 40
320
405
  and len(content.strip().split()) >= _MIN_VERBATIM_WORDS):
321
- import uuid
322
406
  import re as _re
323
407
  _verbatim_text = content.strip()
324
408
  # Extract entities using the same regex as fact_extractor
@@ -362,7 +446,6 @@ def run_store(
362
446
  # their data should NEVER be silently dropped. The min-length and min-word filters
363
447
  # are designed for automatic conversation extraction, not explicit user storage.
364
448
  if not facts and content.strip():
365
- import uuid
366
449
  facts = [AtomicFact(
367
450
  fact_id=uuid.uuid4().hex[:16],
368
451
  content=content.strip(),
@@ -391,6 +474,22 @@ def run_store(
391
474
  temporal_parser=temporal_parser,
392
475
  )
393
476
 
477
+ if (
478
+ materialization_progress is not None
479
+ and not materialization_progress.get("relational_started", False)
480
+ ):
481
+ if materialization_checkpoint is not None:
482
+ materialization_checkpoint(
483
+ "relational_started",
484
+ (),
485
+ {
486
+ "pipeline_started": True,
487
+ "relational_started": True,
488
+ "pipeline": False,
489
+ },
490
+ )
491
+ materialization_progress["relational_started"] = True
492
+
394
493
  is_queryable_promotion = fact.fact_id in queryable_ids
395
494
  if is_queryable_promotion:
396
495
  db.update_fact(fact.fact_id, {
@@ -503,6 +602,8 @@ def run_store(
503
602
 
504
603
  if fact.fact_id not in stored_ids:
505
604
  stored_ids.append(fact.fact_id)
605
+ if materialization_progress is not None:
606
+ materialization_progress["fact_ids"] = tuple(stored_ids)
506
607
 
507
608
  # Dual-write embedding to ANN index + vector store (embed on-demand if
508
609
  # a consolidated ADD fact arrived without one). See _upsert_fact_vectors.
@@ -583,16 +684,33 @@ def run_store(
583
684
  fact.fact_id, exc,
584
685
  )
585
686
 
687
+ # Phase 4b: fact-augmented key expansion (T3b). Index entity aliases /
688
+ # canonical names as BM25 alt-keys so paraphrased queries match. Mode A
689
+ # (entity graph) is zero-LLM and safe on the hot store path; LLM
690
+ # paraphrase enrichment (Mode B/C) is left to background consolidation.
691
+ try:
692
+ from superlocalmemory.core.key_expander import KeyExpander
693
+ _alt_keys = KeyExpander(db).expand(fact, profile_id, mode="a")
694
+ if _alt_keys:
695
+ db.upsert_fact_expansion(fact.fact_id, _alt_keys)
696
+ except Exception as exc:
697
+ logger.debug("Key expansion skipped for %s: %s", fact.fact_id, exc)
698
+
586
699
  if observation_builder:
587
700
  for eid in fact.canonical_entities:
588
701
  observation_builder.update_profile(eid, fact, profile_id)
589
702
 
590
- # Increment fact_count for each linked canonical entity
703
+ # The normalized association key makes this O(1) and exactly-once
704
+ # across retries, crashes, and separate operations that consolidate to
705
+ # the same fact.
591
706
  for eid in fact.canonical_entities:
592
- try:
593
- db.increment_entity_fact_count(eid)
594
- except Exception:
595
- pass # Non-critical — entity may have been deleted
707
+ _record_fact_entity_association(
708
+ db,
709
+ operation_id=ingestion_operation_id,
710
+ profile_id=profile_id,
711
+ fact_id=fact.fact_id,
712
+ entity_id=eid,
713
+ )
596
714
  if scene_builder:
597
715
  scene_builder.assign_to_scene(fact, profile_id)
598
716
 
@@ -603,6 +721,13 @@ def run_store(
603
721
  from superlocalmemory.storage.models import TemporalEvent
604
722
  for eid in fact.canonical_entities:
605
723
  event = TemporalEvent(
724
+ event_id=_ingestion_effect_id(
725
+ ingestion_operation_id,
726
+ "temporal",
727
+ fact.fact_id,
728
+ eid,
729
+ "observed",
730
+ ),
606
731
  profile_id=profile_id, entity_id=eid,
607
732
  fact_id=fact.fact_id,
608
733
  scope=fact.scope,
@@ -620,8 +745,16 @@ def run_store(
620
745
  from superlocalmemory.encoding.foresight import extract_foresight_signals
621
746
  from superlocalmemory.storage.models import TemporalEvent as _TE
622
747
  foresight_signals = extract_foresight_signals(fact)
623
- for sig in foresight_signals:
748
+ for signal_index, sig in enumerate(foresight_signals):
624
749
  f_event = _TE(
750
+ event_id=_ingestion_effect_id(
751
+ ingestion_operation_id,
752
+ "temporal",
753
+ fact.fact_id,
754
+ sig.get("entity_id", ""),
755
+ "foresight",
756
+ signal_index,
757
+ ),
625
758
  profile_id=profile_id,
626
759
  entity_id=sig.get("entity_id", ""),
627
760
  fact_id=fact.fact_id,
@@ -655,11 +788,6 @@ def run_store(
655
788
 
656
789
  logger.info("Stored %d facts (session=%s)", len(stored_ids), session_id)
657
790
 
658
- # Post-operation hooks (audit, trust signal, event bus)
659
- hook_ctx["fact_ids"] = stored_ids
660
- hook_ctx["fact_count"] = len(stored_ids)
661
- hooks.run_post("store", hook_ctx)
662
-
663
791
  if derivation_report is not None:
664
792
  derivation_report.update({
665
793
  "extraction": extraction_complete,
@@ -670,6 +798,35 @@ def run_store(
670
798
  "provenance": provenance_complete,
671
799
  })
672
800
 
801
+ # Stage observations and emitted fact IDs are recorded before optional
802
+ # post-hooks. A hook failure must not erase the durable retry boundary and
803
+ # cause extraction/consolidation to run again.
804
+ if materialization_progress is not None:
805
+ materialization_progress["fact_ids"] = tuple(stored_ids)
806
+ materialization_progress["relational_complete"] = True
807
+ materialization_progress["post_hooks"] = False
808
+ if materialization_checkpoint is not None and stored_ids:
809
+ materialization_checkpoint(
810
+ "relational_complete",
811
+ tuple(stored_ids),
812
+ {
813
+ "pipeline_started": True,
814
+ "relational_started": True,
815
+ "pipeline": True,
816
+ "post_hooks": False,
817
+ "relational": True,
818
+ **dict(derivation_report or {}),
819
+ },
820
+ )
821
+
822
+ # Post-operation hooks (audit, trust signal, event bus)
823
+ hook_ctx["ingestion_operation_id"] = ingestion_operation_id
824
+ hook_ctx["fact_ids"] = stored_ids
825
+ hook_ctx["fact_count"] = len(stored_ids)
826
+ hooks.run_post("store", hook_ctx)
827
+ if materialization_progress is not None:
828
+ materialization_progress["post_hooks"] = True
829
+
673
830
  # Phase 5: Step-count trigger for lightweight consolidation (L7)
674
831
  if consolidation_engine is not None:
675
832
  try:
@@ -70,6 +70,7 @@ class WorkerPool:
70
70
  fast: bool = False,
71
71
  include_global: bool | None = None,
72
72
  include_shared: bool | None = None,
73
+ window: str | None = None,
73
74
  ) -> dict:
74
75
  """Run recall in worker subprocess. Returns result dict.
75
76
 
@@ -91,6 +92,8 @@ class WorkerPool:
91
92
  msg["include_global"] = bool(include_global)
92
93
  if include_shared is not None:
93
94
  msg["include_shared"] = bool(include_shared)
95
+ if window:
96
+ msg["window"] = window
94
97
  return self._send(msg)
95
98
 
96
99
  def store(self, content: str, metadata: dict | None = None) -> dict:
@@ -291,15 +294,27 @@ class WorkerPool:
291
294
  self._idle_timer.cancel()
292
295
  self._idle_timer = None
293
296
  if self._proc is not None:
294
- pid = self._proc.pid
297
+ proc = self._proc
298
+ pid = proc.pid
295
299
  try:
296
- self._proc.stdin.write('{"cmd":"quit"}\n')
297
- self._proc.stdin.flush()
298
- self._proc.wait(timeout=3)
300
+ proc.stdin.write('{"cmd":"quit"}\n')
301
+ proc.stdin.flush()
302
+ proc.wait(timeout=3)
299
303
  except Exception:
300
304
  try:
301
- self._proc.kill()
302
- self._proc.wait(timeout=2)
305
+ proc.kill()
306
+ proc.wait(timeout=2)
307
+ except Exception:
308
+ pass
309
+ # L-CONC-1: deterministically close the pipe fds rather than leaving
310
+ # them to GC. This releases the OS handles immediately and unblocks
311
+ # any orphaned _readline_with_timeout reader thread (its readline
312
+ # returns/raises on the closed pipe), so repeated request timeouts
313
+ # cannot accumulate reader threads or file descriptors. Cross-platform.
314
+ for stream in (proc.stdin, proc.stdout, proc.stderr):
315
+ try:
316
+ if stream is not None:
317
+ stream.close()
303
318
  except Exception:
304
319
  pass
305
320
  self._proc = None
@@ -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]