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
@@ -17,6 +17,7 @@ License: AGPL-3.0-or-later
17
17
  from __future__ import annotations
18
18
 
19
19
  import concurrent.futures
20
+ import functools
20
21
  import logging
21
22
  import math
22
23
  import re
@@ -26,6 +27,11 @@ from typing import TYPE_CHECKING, Any, Protocol
26
27
 
27
28
  from superlocalmemory.core.config import ChannelWeights, RetrievalConfig
28
29
  from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
30
+ from superlocalmemory.retrieval.time_window import (
31
+ in_window,
32
+ infer_window_from_query,
33
+ parse_window,
34
+ )
29
35
  from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
30
36
  from superlocalmemory.storage.models import (
31
37
  AtomicFact,
@@ -97,12 +103,11 @@ class RetrievalEngine:
97
103
  self._profile_channel = profile_channel
98
104
  self._bridge = bridge_discovery
99
105
  self._trust_scorer = trust_scorer
100
- # v3.6.15: serialise the per-recall scope-flag set + channel execution.
101
- # Channel instances are SHARED across concurrent recalls (the daemon runs
102
- # several in parallel); without this, recall B's flags could overwrite
103
- # recall A's mid-flight on the shared channels. Uncontended for a single
104
- # recall (~0 cost); only the channel phase of concurrent recalls serialises.
105
- self._scope_lock = threading.Lock()
106
+ # v3.7.9: scope flags (include_global / include_shared) are now threaded
107
+ # as explicit call parameters into every channel's search() method, so
108
+ # concurrent recalls each carry their own flags no shared mutable state,
109
+ # no lock needed. The _scope_lock and per-recall attribute-set loop have
110
+ # been removed. See defect S01 in the fix/3.7.9 branch notes.
106
111
  # One executor belongs to one retrieval engine. Creating/destroying six
107
112
  # worker threads on every recall caused allocator/thread-stack RSS churn
108
113
  # under sustained sessions. The scope lock already serializes channel
@@ -149,6 +154,7 @@ class RetrievalEngine:
149
154
  extra_disabled_channels: set[str] | None = None,
150
155
  include_global: bool = False,
151
156
  include_shared: bool = False,
157
+ window: str | tuple[str, str] | None = None,
152
158
  ) -> RecallResponse:
153
159
  """Full retrieval pipeline: strategy -> channels -> RRF -> rerank.
154
160
 
@@ -161,7 +167,11 @@ class RetrievalEngine:
161
167
  for the ``--fast`` CLI flag) without mutating shared config.
162
168
  """
163
169
  t0 = time.monotonic()
164
- self._extra_disabled = set(extra_disabled_channels or ())
170
+ # NOTE: extra_disabled_channels is passed as an explicit local argument
171
+ # to _run_channels() — it is NOT stored on self. Storing it as a shared
172
+ # mutable instance attribute (the old self._extra_disabled = ...) caused
173
+ # a race condition where two concurrent recalls could overwrite each
174
+ # other's channel-disable set (v3.4.64 fix).
165
175
 
166
176
  # v3.5.0 diagnostic: stage timing inside retrieval (SLM_RECALL_TIMING=1).
167
177
  import os as _os_e
@@ -195,18 +205,14 @@ class RetrievalEngine:
195
205
  # Dynamic top-k for aggregation queries
196
206
  effective_limit = 100 if strat.query_type == "aggregation" else limit
197
207
 
198
- # 3. Run channels. Set the scope flags on the shared channel instances
199
- # and execute them under self._scope_lock so a concurrent recall can't
200
- # interleave its scope visibility onto these channels mid-flight. The
201
- # worker threads spawned inside _run_channels are joined before the lock
202
- # releases, so every channel read sees THIS recall's flags.
203
- with self._scope_lock:
204
- for ch in (self._semantic, self._bm25, self._entity, self._temporal,
205
- self._hopfield, self._spreading_activation):
206
- if ch is not None:
207
- ch.include_global = include_global
208
- ch.include_shared = include_shared
209
- ch_results = self._run_channels(query, profile_id, strat)
208
+ # 3. Run channels. Both scope flags AND extra_disabled_channels travel as
209
+ # explicit call parameters so concurrent recalls with different flags
210
+ # cannot corrupt each other. No lock needed no shared mutable state.
211
+ ch_results = self._run_channels(
212
+ query, profile_id, strat,
213
+ extra_disabled_channels=extra_disabled_channels,
214
+ include_global=include_global, include_shared=include_shared,
215
+ )
210
216
  _em("run_channels")
211
217
  if profile_hits:
212
218
  ch_results["profile"] = profile_hits
@@ -318,6 +324,32 @@ class RetrievalEngine:
318
324
  logger.warning("Entity graph signal enhancement: %s", exc)
319
325
 
320
326
  _em("expand+entity_enh")
327
+
328
+ # T-window: prune candidates to the requested event-time range.
329
+ # Event times are fetched for the bounded candidate set only (indexed),
330
+ # then in-range facts are kept — before fact load, so out-of-window facts
331
+ # are never materialized. T3: when the caller passes no explicit window,
332
+ # infer one from natural-language scope in the query ("last week").
333
+ # Safety: an EXPLICIT window is authoritative (honoured even if it empties
334
+ # the set — the user asked for that scope), but an INFERRED window is
335
+ # additive and never makes recall worse — if it would empty the results,
336
+ # fall back to the unwindowed set.
337
+ _explicit_window = window is not None
338
+ _window = window if _explicit_window else infer_window_from_query(query)
339
+ if _window is not None and fused:
340
+ bounds = parse_window(_window)
341
+ if bounds is not None:
342
+ etimes = self._db.get_fact_event_times(
343
+ [fr.fact_id for fr in fused], profile_id,
344
+ )
345
+ windowed = [
346
+ fr for fr in fused
347
+ if in_window(etimes.get(fr.fact_id), bounds)
348
+ ]
349
+ if windowed or _explicit_window:
350
+ fused = windowed
351
+ _em("time_window")
352
+
321
353
  # 4. Load facts for rerank pool
322
354
  pool = min(len(fused), max(effective_limit * 3, 30))
323
355
  top = fused[:pool]
@@ -411,8 +443,85 @@ class RetrievalEngine:
411
443
  no_confident_match=no_match,
412
444
  reranker_applied=reranker_applied,
413
445
  reranker_status=reranker_status,
446
+ # Q2b: thematic context when the top results cluster in one
447
+ # community. Precomputed summary lookup only — no per-query LLM.
448
+ community_context=self._community_context(results, profile_id),
414
449
  )
415
450
 
451
+ # -- Community context (Wave Q2b) --------------------------------------
452
+
453
+ def _community_context(
454
+ self, results: list[Any], profile_id: str, top_k: int = 8,
455
+ ) -> dict | None:
456
+ """Attach the precomputed community summary the top results fall into.
457
+
458
+ On-device-safe (market CRIT-1): a single read of the ≤N precomputed
459
+ community_summaries rows + a membership tally — never a per-query LLM
460
+ fan-out. Gated: fires only when >=2 of the top results AND >=40% of
461
+ them belong to one community, so precise factual queries are untouched.
462
+ Fail-open: any error returns None (recall is never affected).
463
+ """
464
+ if not results or not getattr(
465
+ self._config, "enable_community_context", True,
466
+ ):
467
+ return None
468
+ try:
469
+ import json
470
+ from collections import Counter
471
+
472
+ rows = [
473
+ dict(r) for r in self._db.execute(
474
+ "SELECT community_id, summary, keywords, fact_ids_json, "
475
+ "fact_count FROM community_summaries WHERE profile_id = ?",
476
+ (profile_id,),
477
+ )
478
+ ]
479
+ if not rows:
480
+ return None
481
+
482
+ fact_to_cid: dict[str, int] = {}
483
+ summ_by_cid: dict[int, dict] = {}
484
+ for r in rows:
485
+ cid = int(r["community_id"])
486
+ summ_by_cid[cid] = r
487
+ try:
488
+ for fid in json.loads(r.get("fact_ids_json") or "[]"):
489
+ fact_to_cid[str(fid)] = cid
490
+ except (ValueError, TypeError):
491
+ continue
492
+
493
+ top_ids = [
494
+ res.fact.fact_id
495
+ for res in results[:top_k]
496
+ if getattr(res, "fact", None) is not None
497
+ ]
498
+ tally = Counter(
499
+ fact_to_cid[fid] for fid in top_ids if fid in fact_to_cid
500
+ )
501
+ if not tally:
502
+ return None
503
+ best_cid, count = tally.most_common(1)[0]
504
+ coverage = count / len(top_ids) if top_ids else 0.0
505
+ if count < 2 or coverage < 0.4:
506
+ return None
507
+
508
+ row = summ_by_cid[best_cid]
509
+ try:
510
+ members = json.loads(row.get("fact_ids_json") or "[]")
511
+ except (ValueError, TypeError):
512
+ members = []
513
+ return {
514
+ "community_id": best_cid,
515
+ "summary": row.get("summary", ""),
516
+ "keywords": row.get("keywords", ""),
517
+ "member_fact_ids": members,
518
+ "coverage": round(coverage, 3),
519
+ "matched_results": count,
520
+ }
521
+ except Exception as exc:
522
+ logger.debug("community context skipped (fail-open): %s", exc)
523
+ return None
524
+
416
525
  # -- Evidence floor (v3.6.6) -------------------------------------------
417
526
 
418
527
  @staticmethod
@@ -570,32 +679,47 @@ class RetrievalEngine:
570
679
  fused: list,
571
680
  ch_results: dict[str, list[tuple[str, float]]],
572
681
  effective_limit: int,
573
- min_per_channel: int = 2,
574
682
  ) -> list:
575
- """Ensure structure channels (entity_graph) get representation.
576
-
577
- V3.4.11: entity_graph finds valid results but RRF scores them low
578
- because they don't overlap with semantic/bm25 results. This interleaves
579
- top entity_graph facts into positions 3-4 of the final output instead
580
- of appending at the end where they'd never be seen.
683
+ """Keep strong lexical and structure evidence visible in the result cap.
684
+
685
+ A semantic channel with a larger weight can fill a small result limit
686
+ even when BM25 has an exact, high-signal hit. That broke the
687
+ ``queryable now`` ingestion contract: a freshly inserted FTS row could
688
+ exist durably but remain invisible to immediate recall. Reserve one
689
+ capped slot for a strong BM25 hit and two for a structure channel when
690
+ such candidates exist, without returning more than ``effective_limit``.
581
691
  """
582
- structure_channels = ["entity_graph"]
692
+ channel_minimums = (
693
+ ("bm25", 1, 0.0),
694
+ ("entity_graph", 2, 0.0),
695
+ )
583
696
  top_ids = {fr.fact_id for fr in top}
584
697
 
585
698
  promoted = []
586
- for ch_name in structure_channels:
699
+ for ch_name, minimum, score_floor in channel_minimums:
587
700
  ch_items = ch_results.get(ch_name, [])
588
701
  if not ch_items:
589
702
  continue
590
703
 
591
- present = sum(1 for fid, _ in ch_items if fid in top_ids)
592
- if present >= min_per_channel:
704
+ eligible_ids = {
705
+ fid
706
+ for fid, score in ch_items
707
+ if (
708
+ float(score) > score_floor
709
+ if ch_name == "bm25"
710
+ else float(score) >= score_floor
711
+ )
712
+ }
713
+ if not eligible_ids:
714
+ continue
715
+
716
+ present = sum(1 for fid in eligible_ids if fid in top_ids)
717
+ if present >= minimum:
593
718
  continue
594
719
 
595
- needed = min_per_channel - present
596
- ch_fids = {fid for fid, _ in ch_items}
720
+ needed = minimum - present
597
721
  for fr in fused:
598
- if fr.fact_id in ch_fids and fr.fact_id not in top_ids:
722
+ if fr.fact_id in eligible_ids and fr.fact_id not in top_ids:
599
723
  promoted.append(fr)
600
724
  top_ids.add(fr.fact_id)
601
725
  needed -= 1
@@ -605,10 +729,15 @@ class RetrievalEngine:
605
729
  if not promoted:
606
730
  return top
607
731
 
608
- # Append as safety net — with proper RRF weights (strategy.py),
609
- # entity_graph facts should already rank naturally in the top-k.
610
- # This only fires when they're still missing despite weight boost.
611
- return list(top) + promoted
732
+ selected = promoted[:effective_limit]
733
+ result = list(top[:effective_limit])
734
+ free_slots = max(0, effective_limit - len(result))
735
+ result.extend(selected[:free_slots])
736
+ remaining = selected[free_slots:]
737
+ if remaining:
738
+ keep = max(0, effective_limit - len(remaining))
739
+ result = result[:keep] + remaining
740
+ return result[:effective_limit]
612
741
 
613
742
  # -- Channel execution --------------------------------------------------
614
743
 
@@ -628,7 +757,14 @@ class RetrievalEngine:
628
757
  return emb
629
758
 
630
759
  def _run_channels(
631
- self, query: str, profile_id: str, strat: QueryStrategy,
760
+ self,
761
+ query: str,
762
+ profile_id: str,
763
+ strat: QueryStrategy,
764
+ *,
765
+ extra_disabled_channels: set[str] | None = None,
766
+ include_global: bool = False,
767
+ include_shared: bool = False,
632
768
  ) -> dict[str, list[tuple[str, float]]]:
633
769
  """Run active retrieval channels.
634
770
 
@@ -646,7 +782,9 @@ class RetrievalEngine:
646
782
  out: dict[str, list[tuple[str, float]]] = {}
647
783
  # Skip channels listed in disabled_channels (ablation support)
648
784
  # V3.4.40: union with per-recall extra_disabled set (e.g. --fast skip)
649
- disabled = set(self._config.disabled_channels) | getattr(self, "_extra_disabled", set())
785
+ # V3.4.64: extra_disabled is now a local parameter, not a shared instance
786
+ # attribute — eliminates the concurrent-recall race condition.
787
+ disabled = set(self._config.disabled_channels) | set(extra_disabled_channels or ())
650
788
 
651
789
  # V3.3.4: Embed query ONCE, reuse for semantic + hopfield channels
652
790
  q_emb: list[float] | None = None
@@ -688,22 +826,38 @@ class RetrievalEngine:
688
826
  if self._semantic is not None and q_emb is not None and "semantic" not in disabled:
689
827
  futures["semantic"] = executor.submit(
690
828
  _safe_channel, "semantic",
691
- self._semantic.search, q_emb, profile_id, self._config.semantic_top_k,
829
+ functools.partial(
830
+ self._semantic.search,
831
+ include_global=include_global, include_shared=include_shared,
832
+ ),
833
+ q_emb, profile_id, self._config.semantic_top_k,
692
834
  )
693
835
  if self._bm25 is not None and "bm25" not in disabled:
694
836
  futures["bm25"] = executor.submit(
695
837
  _safe_channel, "bm25",
696
- self._bm25.search, query, profile_id, self._config.bm25_top_k,
838
+ functools.partial(
839
+ self._bm25.search,
840
+ include_global=include_global, include_shared=include_shared,
841
+ ),
842
+ query, profile_id, self._config.bm25_top_k,
697
843
  )
698
844
  if self._temporal is not None and "temporal" not in disabled:
699
845
  futures["temporal"] = executor.submit(
700
846
  _safe_channel, "temporal",
701
- self._temporal.search, query, profile_id, self._config.bm25_top_k,
847
+ functools.partial(
848
+ self._temporal.search,
849
+ include_global=include_global, include_shared=include_shared,
850
+ ),
851
+ query, profile_id, self._config.bm25_top_k,
702
852
  )
703
853
  if self._hopfield is not None and q_emb is not None and "hopfield" not in disabled:
704
854
  futures["hopfield"] = executor.submit(
705
855
  _safe_channel, "hopfield",
706
- self._hopfield.search, q_emb, profile_id, self._config.hopfield_top_k,
856
+ functools.partial(
857
+ self._hopfield.search,
858
+ include_global=include_global, include_shared=include_shared,
859
+ ),
860
+ q_emb, profile_id, self._config.hopfield_top_k,
707
861
  )
708
862
  if (
709
863
  self._spreading_activation is not None
@@ -712,17 +866,33 @@ class RetrievalEngine:
712
866
  ):
713
867
  futures["spreading_activation"] = executor.submit(
714
868
  _safe_channel, "spreading_activation",
715
- self._spreading_activation.search, q_emb, profile_id, self._config.bm25_top_k,
869
+ functools.partial(
870
+ self._spreading_activation.search,
871
+ include_global=include_global, include_shared=include_shared,
872
+ ),
873
+ q_emb, profile_id, self._config.bm25_top_k,
716
874
  )
717
875
 
718
- # Collect results as channels complete.
876
+ # Each local channel gets a strict latency budget. A slow graph walk
877
+ # must not make an interactive recall wait 30 seconds; completed
878
+ # channels still participate in fusion and the timeout is observable.
879
+ channel_timeout_seconds = 1.0
880
+ # One shared deadline keeps parallel dispatch genuinely bounded. A
881
+ # per-future timeout here would serialise the wait and turn five slow
882
+ # channels into five seconds of UI latency.
883
+ done, pending = concurrent.futures.wait(
884
+ futures.values(), timeout=channel_timeout_seconds,
885
+ )
719
886
  for name, fut in futures.items():
887
+ if fut in pending:
888
+ logger.warning("Channel %s exceeded %.1fs latency budget", name, channel_timeout_seconds)
889
+ continue
720
890
  try:
721
- ch_name, result = fut.result(timeout=30)
891
+ ch_name, result = fut.result()
722
892
  if result:
723
893
  out[ch_name] = result
724
894
  except Exception as exc:
725
- logger.warning("Channel %s timed out or failed: %s", name, exc)
895
+ logger.warning("Channel %s failed: %s", name, exc)
726
896
 
727
897
  # Apply registered post-retrieval filters (forgetting filter, etc.)
728
898
  if hasattr(self, '_registry') and self._registry._filters:
@@ -981,6 +1151,10 @@ class RetrievalEngine:
981
1151
 
982
1152
  _CHANNEL_KEYS: tuple[str, ...] = (
983
1153
  "semantic", "bm25", "entity_graph", "temporal",
1154
+ # hopfield + spreading_activation are real retrieval channels (score
1155
+ # contract v2) with bandit-chosen weights; omitting them here silently
1156
+ # discarded adaptive reranking for multi-hop relational recall.
1157
+ "spreading_activation", "hopfield",
984
1158
  )
985
1159
 
986
1160
 
@@ -835,13 +835,15 @@ class EntityGraphChannel:
835
835
  )[:top_k]
836
836
  # Shadow SQLite before accepting a projected answer. The graph
837
837
  # channel has optional PageRank/community enrichments, so exact
838
- # score equality is neither required nor useful; result ordering
839
- # and membership are the correctness contract. Any divergence is
840
- # recorded and fails closed to canonical SQLite.
838
+ # Score equality is neither required nor useful; result *membership*
839
+ # is the correctness contract. Order within the same fact set is
840
+ # tolerated requiring identical ordering would fail closed on
841
+ # every query with score ties, leaving Cozo permanently unused.
842
+ # Any membership divergence is recorded and fails closed to SQLite.
841
843
  sqlite_results = self._search_without_cozo(query, profile_id, top_k)
842
- matches = [fact_id for fact_id, _ in cozo_results] == [
844
+ matches = {fact_id for fact_id, _ in cozo_results} == {
843
845
  fact_id for fact_id, _ in sqlite_results
844
- ]
846
+ }
845
847
  record = getattr(self._cozo, "record_shadow_comparison", None)
846
848
  if callable(record):
847
849
  record(matches=matches, projected=cozo_results, canonical=sqlite_results)
@@ -88,6 +88,8 @@ class HopfieldChannel:
88
88
  query: Any,
89
89
  profile_id: str,
90
90
  top_k: int = 50,
91
+ include_global: bool | None = None,
92
+ include_shared: bool | None = None,
91
93
  ) -> list[tuple[str, float]]:
92
94
  """Search for facts using Hopfield associative retrieval.
93
95
 
@@ -95,6 +97,9 @@ class HopfieldChannel:
95
97
  query: Query embedding (list[float] or np.ndarray).
96
98
  profile_id: Scope search to this profile.
97
99
  top_k: Maximum results to return.
100
+ include_global: Include global-scope facts. Falls back to the
101
+ instance attribute when not supplied.
102
+ include_shared: Include shared-scope facts. Same fallback.
98
103
 
99
104
  Returns:
100
105
  List of (fact_id, score) sorted by score descending.
@@ -104,8 +109,10 @@ class HopfieldChannel:
104
109
  if not self._config.enabled:
105
110
  return []
106
111
 
107
- include_global = bool(getattr(self, "include_global", False))
108
- include_shared = bool(getattr(self, "include_shared", False))
112
+ if include_global is None:
113
+ include_global = bool(getattr(self, "include_global", False))
114
+ if include_shared is None:
115
+ include_shared = bool(getattr(self, "include_shared", False))
109
116
  try:
110
117
  with self._cache_lock:
111
118
  return self._search_inner(
@@ -25,9 +25,8 @@ import sys
25
25
  import threading
26
26
  import time
27
27
  import weakref
28
- from typing import Any
29
-
30
28
  from pathlib import Path
29
+ from typing import Any
31
30
 
32
31
  from superlocalmemory.infra.data_root import state_path
33
32
  from superlocalmemory.storage.models import AtomicFact
@@ -57,9 +56,9 @@ _live_rerankers: set[weakref.ref] = set()
57
56
 
58
57
  logger = logging.getLogger(__name__)
59
58
 
60
- _IDLE_TIMEOUT_SECONDS = 300 # V3.4.37: 5 min (was 30) — balance cold-start vs RAM.
59
+ _IDLE_TIMEOUT_SECONDS = 1800 # V3.8.1: keep interactive sessions warm.
61
60
  # V3.3.12: Configurable via SLM_RERANKER_IDLE_TIMEOUT env var.
62
- # V3.4.19: Bumped from 120 1800 in lock-step with the embedding worker.
61
+ # Low-RAM installations can retain aggressive recycling through the override.
63
62
  # Set ``SLM_RERANKER_IDLE_TIMEOUT=120`` + ``slm restart`` to revert.
64
63
  _IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_RERANKER_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
65
64
  _SUBPROCESS_RESPONSE_TIMEOUT = 15 # v3.4.52: 15s (was 180s). Long timeout blocked the