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
@@ -7,8 +7,8 @@
7
7
  All PyTorch/model work runs in a SEPARATE subprocess. The main process
8
8
  (dashboard, MCP, CLI) never imports torch and stays at ~60 MB.
9
9
 
10
- The worker subprocess auto-kills after 2 minutes idle, returning all
11
- memory to the OS. It respawns on next embed call (~3 sec cold start).
10
+ The worker subprocess has a configurable idle timeout and respawns on the
11
+ next embed call when it has been unloaded.
12
12
 
13
13
  Part of Qualixar | Author: Varun Pratap Bhardwaj
14
14
  """
@@ -29,14 +29,14 @@ from typing import TYPE_CHECKING
29
29
 
30
30
  import numpy as np
31
31
 
32
+ from superlocalmemory.core.config import EmbeddingConfig
33
+
32
34
  # Track all live embedding services for atexit cleanup
33
35
  _live_embedding_services: set[weakref.ref] = set()
34
36
 
35
37
  if TYPE_CHECKING:
36
38
  from numpy.typing import NDArray
37
39
 
38
- from superlocalmemory.core.config import EmbeddingConfig
39
-
40
40
  logger = logging.getLogger(__name__)
41
41
 
42
42
  # Fisher variance constants
@@ -63,6 +63,7 @@ class DimensionMismatchError(RuntimeError):
63
63
 
64
64
  _MAX_CONCURRENT_WORKERS = int(os.environ.get("SLM_MAX_EMBEDDING_WORKERS", 1))
65
65
  _embedding_lock_fd: int | None = None
66
+ _embedding_lock_state_guard = threading.Lock()
66
67
 
67
68
 
68
69
  def _embedding_lock_file() -> Path:
@@ -106,57 +107,87 @@ def register_embedding_worker_pid(pid: int) -> None:
106
107
  def acquire_embedding_lock(timeout: float = 5.0) -> bool:
107
108
  """Acquire system-wide embedding worker lock.
108
109
 
109
- v3.4.13: First checks if a worker PID is already alive (fast path).
110
- Falls back to fcntl.flock on Unix. On Windows, falls back to PID check only.
110
+ The caller must re-check the PID file after acquisition before spawning.
111
+ POSIX uses flock; Windows uses a one-byte msvcrt lock.
111
112
  Returns True if lock acquired (safe to spawn), False if another worker active.
112
113
  """
113
114
  global _embedding_lock_fd
114
115
 
115
- # v3.4.13: Fast path if a worker PID is alive, don't even try the lock
116
- if _is_embedding_worker_alive():
117
- return False
118
-
119
- if sys.platform == "win32":
120
- return True # No file locking on Windows — PID check above is the guard
121
-
122
- import fcntl
123
- lock_file = _embedding_lock_file()
124
- lock_file.parent.mkdir(parents=True, exist_ok=True)
116
+ # Serialize local contenders as well as cross-process contenders. The file
117
+ # descriptor stays local until its OS lock succeeds, so a failed acquire
118
+ # can never overwrite and leak the descriptor that owns the live worker.
119
+ with _embedding_lock_state_guard:
120
+ if _embedding_lock_fd is not None:
121
+ return False
122
+ if _is_embedding_worker_alive():
123
+ return False
125
124
 
126
- try:
127
- _embedding_lock_fd = os.open(str(lock_file), os.O_CREAT | os.O_RDWR)
128
- deadline = time.time() + timeout
129
- while time.time() < deadline:
130
- try:
131
- fcntl.flock(_embedding_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
132
- return True
133
- except (BlockingIOError, OSError):
134
- time.sleep(0.2)
135
- # Timeout — another worker holds the lock
136
- os.close(_embedding_lock_fd)
137
- _embedding_lock_fd = None
138
- return False
139
- except Exception:
140
- return True # On error, allow through (don't block functionality)
125
+ lock_file = _embedding_lock_file()
126
+ lock_file.parent.mkdir(parents=True, exist_ok=True)
127
+ candidate_fd: int | None = None
128
+ try:
129
+ candidate_fd = os.open(str(lock_file), os.O_CREAT | os.O_RDWR)
130
+ deadline = time.time() + timeout
131
+ while time.time() < deadline:
132
+ try:
133
+ if sys.platform == "win32":
134
+ import msvcrt
135
+
136
+ if os.fstat(candidate_fd).st_size == 0:
137
+ os.write(candidate_fd, b"\0")
138
+ os.lseek(candidate_fd, 0, os.SEEK_SET)
139
+ msvcrt.locking(candidate_fd, msvcrt.LK_NBLCK, 1)
140
+ else:
141
+ import fcntl
142
+
143
+ fcntl.flock(
144
+ candidate_fd,
145
+ fcntl.LOCK_EX | fcntl.LOCK_NB,
146
+ )
147
+ _embedding_lock_fd = candidate_fd
148
+ return True
149
+ except (BlockingIOError, OSError):
150
+ time.sleep(0.2)
151
+ os.close(candidate_fd)
152
+ return False
153
+ except Exception:
154
+ if candidate_fd is not None:
155
+ try:
156
+ os.close(candidate_fd)
157
+ except OSError:
158
+ pass
159
+ return False
141
160
 
142
161
 
143
162
  def release_embedding_lock() -> None:
144
163
  """Release system-wide embedding worker lock."""
145
164
  global _embedding_lock_fd
146
- if _embedding_lock_fd is not None:
165
+ with _embedding_lock_state_guard:
166
+ if _embedding_lock_fd is None:
167
+ return
147
168
  try:
148
- import fcntl
149
- fcntl.flock(_embedding_lock_fd, fcntl.LOCK_UN)
169
+ if sys.platform == "win32":
170
+ import msvcrt
171
+
172
+ os.lseek(_embedding_lock_fd, 0, os.SEEK_SET)
173
+ msvcrt.locking(_embedding_lock_fd, msvcrt.LK_UNLCK, 1)
174
+ else:
175
+ import fcntl
176
+
177
+ fcntl.flock(_embedding_lock_fd, fcntl.LOCK_UN)
150
178
  os.close(_embedding_lock_fd)
151
179
  except Exception:
152
- pass
180
+ try:
181
+ os.close(_embedding_lock_fd)
182
+ except OSError:
183
+ pass
153
184
  _embedding_lock_fd = None
154
185
 
155
186
 
156
- _IDLE_TIMEOUT_SECONDS = 300 # 5 minutes — balance cold-start vs RAM.
157
- # V3.4.37: Reduced from 1800 300. Holding 1.1 GB for 30 min idle
158
- # wastes RAM on laptops. 5 min covers bursty session_init+recall
159
- # patterns while freeing memory between sessions.
187
+ _IDLE_TIMEOUT_SECONDS = 1800 # 30 minutes — keep interactive sessions warm.
188
+ # V3.8.1: existing-user soaks showed that the five-minute policy repeatedly
189
+ # recycled a ~1.1 GB local model and imposed 20-30 second cold starts. The
190
+ # explicit environment override remains available for low-RAM installations.
160
191
  _IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_EMBED_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
161
192
  # V3.3.21: Configurable response timeout — 180s default, but batch ingestion
162
193
  # (2-turn chunks across 10 conversations) needs 600s+ to survive cold-start
@@ -186,6 +217,7 @@ class EmbeddingService:
186
217
  self._last_used: float = 0.0
187
218
  self._idle_timer: threading.Timer | None = None
188
219
  self._worker_ready = False
220
+ self._owns_worker_lock = False
189
221
  self._request_count: int = 0
190
222
  self._http_client: object | None = None
191
223
 
@@ -295,7 +327,13 @@ class EmbeddingService:
295
327
  never hangs indefinitely on cold model loads or network issues.
296
328
  """
297
329
  with self._lock:
298
- if not self._available:
330
+ # Only an explicit terminal disable (``False``) short-circuits. A
331
+ # ``None`` availability is the recall-health self-heal's "re-probe"
332
+ # signal (recall_health._heal_embedder) — it must fall through and
333
+ # respawn the worker, matching OllamaEmbedder's tri-state
334
+ # convention. Using ``not self._available`` here bricked the local
335
+ # worker on the first heal tick, because ``None`` is falsy.
336
+ if self._available is False:
299
337
  return None
300
338
  # Worker recycling: restart after N requests to prevent
301
339
  # C++ allocator fragmentation over long-running sessions.
@@ -348,6 +386,11 @@ class EmbeddingService:
348
386
  self._available = False
349
387
  self._kill_worker()
350
388
  return None
389
+ # A successful embed proves the worker is healthy, so clear any
390
+ # transient/``None`` availability left by a self-heal re-probe
391
+ # back to a definite ``True``. Without this the flag lingers at
392
+ # ``None`` and the next ``not``-style check elsewhere re-blocks.
393
+ self._available = True
351
394
  self._reset_idle_timer()
352
395
  self._request_count += 1
353
396
  return resp["vectors"]
@@ -467,18 +510,32 @@ class EmbeddingService:
467
510
  v3.4.13: Machine-wide singleton — checks PID file before spawning.
468
511
  Only ONE embedding_worker can exist at a time on the machine.
469
512
  """
470
- if self._worker_proc is not None and self._worker_proc.poll() is None:
471
- return
472
- self._worker_proc = None
513
+ if self._worker_proc is not None:
514
+ if self._worker_proc.poll() is None:
515
+ return
516
+ # An unexpectedly exited child still leaves this service holding
517
+ # its lifetime flock. Fully close the dead process and release that
518
+ # lock before attempting the normal acquire/spawn sequence. Merely
519
+ # dropping the Popen reference makes the process deadlock against
520
+ # its own old flock until the acquire timeout expires.
521
+ self._kill_worker()
473
522
 
474
- # v3.4.13: Check if another worker is already alive (machine-wide)
523
+ # Serialize the check/spawn/register sequence across processes. Checking
524
+ # the PID file without this lock allows two cold callers to both see no
525
+ # worker and launch memory-heavy children.
526
+ if not acquire_embedding_lock():
527
+ logger.debug("Embedding worker owned by another process")
528
+ self._available = False
529
+ return
475
530
  if _is_embedding_worker_alive():
476
- logger.debug("Embedding worker already alive (PID file), skipping spawn")
531
+ release_embedding_lock()
532
+ logger.debug("Embedding worker already alive after lock acquisition")
477
533
  self._available = False
478
534
  return
479
535
 
480
536
  # V3.3.28: Check memory pressure before spawning
481
537
  if not self._check_memory_pressure():
538
+ release_embedding_lock()
482
539
  logger.warning("Skipping embedding worker spawn due to memory pressure")
483
540
  self._available = False
484
541
  return
@@ -513,9 +570,23 @@ class EmbeddingService:
513
570
  )
514
571
  # v3.4.13: Register PID for machine-wide singleton guard
515
572
  register_embedding_worker_pid(self._worker_proc.pid)
573
+ self._owns_worker_lock = True
516
574
  logger.info("Embedding worker spawned (PID %d)", self._worker_proc.pid)
517
575
  self._worker_ready = True
518
576
  except Exception as exc:
577
+ failed_proc = self._worker_proc
578
+ if failed_proc is not None:
579
+ try:
580
+ failed_proc.terminate()
581
+ failed_proc.wait(timeout=3)
582
+ except Exception:
583
+ try:
584
+ failed_proc.kill()
585
+ failed_proc.wait(timeout=3)
586
+ except Exception:
587
+ pass
588
+ release_embedding_lock()
589
+ self._owns_worker_lock = False
519
590
  logger.warning(
520
591
  "Failed to spawn embedding worker: %s. "
521
592
  "Run 'slm doctor' to verify your Python environment. "
@@ -565,9 +636,23 @@ class EmbeddingService:
565
636
  stream.close()
566
637
  except (BrokenPipeError, OSError, ValueError):
567
638
  pass
639
+ if getattr(self, "_owns_worker_lock", False):
640
+ try:
641
+ pid_file = _embedding_pid_file()
642
+ if (
643
+ proc is not None
644
+ and pid_file.exists()
645
+ and pid_file.read_text().strip() == str(proc.pid)
646
+ ):
647
+ pid_file.unlink(missing_ok=True)
648
+ except (OSError, ValueError):
649
+ pass
650
+ finally:
651
+ self._owns_worker_lock = False
652
+ release_embedding_lock()
568
653
 
569
654
  def _reset_idle_timer(self) -> None:
570
- """Reset idle timer — kills worker after 2 min inactivity."""
655
+ """Reset the configurable worker-idle timer."""
571
656
  if self._idle_timer is not None:
572
657
  self._idle_timer.cancel()
573
658
  self._idle_timer = threading.Timer(
@@ -180,6 +180,15 @@ class MemoryEngine:
180
180
  except Exception as exc:
181
181
  logger.warning("V3.4.6 schema migration failed: %s", exc)
182
182
 
183
+ # V3.4.7: Apply "Learning Brain" schema before deferred migrations.
184
+ # M029 adds composite history indexes to these runtime tables, so the
185
+ # tables must exist before the migration runner records M029 complete.
186
+ try:
187
+ from superlocalmemory.storage.schema_v347 import apply_v347_schema
188
+ apply_v347_schema(str(self._db.db_path))
189
+ except Exception as exc:
190
+ logger.warning("V3.4.7 schema migration failed: %s", exc)
191
+
183
192
  # v3.6.15: apply ALL pending migrations — including DEFERRED ones like
184
193
  # M016 (scope/shared_with columns) — for DIRECT-engine usage: `slm
185
194
  # remember --sync`, the Python API, and LangChain/CrewAI integrations.
@@ -213,13 +222,6 @@ class MemoryEngine:
213
222
  f"ingestion migration failed: {exc}"
214
223
  ) from exc
215
224
 
216
- # V3.4.7: Apply "Learning Brain" schema (tool_events, behavioral_assertions)
217
- try:
218
- from superlocalmemory.storage.schema_v347 import apply_v347_schema
219
- apply_v347_schema(str(self._db.db_path))
220
- except Exception as exc:
221
- logger.warning("V3.4.7 schema migration failed: %s", exc)
222
-
223
225
  # V3.4.10: Apply "Fortress" schema (backup_destinations, entity_blacklist)
224
226
  try:
225
227
  from superlocalmemory.storage.schema_v3410 import apply_v3410_schema
@@ -276,6 +278,18 @@ class MemoryEngine:
276
278
  )
277
279
  self._llm = None
278
280
 
281
+ # H-03 (3.7.9): surface mode-capability degradation explicitly at
282
+ # startup. validate_mode_config existed but was never called, so Mode B
283
+ # silently using rule-based extraction after Ollama vanished (update,
284
+ # restart, port conflict) went unwarned. Each mode only checks its own
285
+ # capabilities, so passing the single llm-availability signal is safe.
286
+ from superlocalmemory.core.modes import validate_mode_config
287
+ _llm_up = getattr(self, "_llm", None) is not None
288
+ for _warning in validate_mode_config(
289
+ self._config.mode, has_ollama=_llm_up, has_cloud_llm=_llm_up,
290
+ ):
291
+ logger.warning("Mode config: %s", _warning)
292
+
279
293
  from superlocalmemory.trust.scorer import TrustScorer
280
294
  from superlocalmemory.trust.provenance import ProvenanceTracker
281
295
  from superlocalmemory.compliance.eu_ai_act import EUAIActChecker
@@ -348,16 +362,17 @@ class MemoryEngine:
348
362
 
349
363
  self._check_embedding_migration()
350
364
 
351
- # V3.3.13: Background maintenance scheduler (Langevin/Ebbinghaus/Sheaf)
352
- if self._config.forgetting.enabled:
353
- try:
354
- from superlocalmemory.core.maintenance_scheduler import MaintenanceScheduler
355
- self._maintenance_scheduler = MaintenanceScheduler(
356
- self._db, self._config, self._profile_id,
357
- )
358
- self._maintenance_scheduler.start()
359
- except Exception as exc:
360
- logger.debug("Maintenance scheduler init failed: %s", exc)
365
+ # Lifecycle/tier evaluation, bounded housekeeping, and backup checks
366
+ # must continue even when optional forgetting/math maintenance is
367
+ # disabled. The scheduler itself gates those optional calculations.
368
+ try:
369
+ from superlocalmemory.core.maintenance_scheduler import MaintenanceScheduler
370
+ self._maintenance_scheduler = MaintenanceScheduler(
371
+ self._db, self._config, self._profile_id,
372
+ )
373
+ self._maintenance_scheduler.start()
374
+ except Exception as exc:
375
+ logger.debug("Maintenance scheduler init failed: %s", exc)
361
376
 
362
377
  def _process_pending_memories(self) -> None:
363
378
  """Process pending memories from store-first async pattern.
@@ -374,7 +389,9 @@ class MemoryEngine:
374
389
  return
375
390
 
376
391
  base_dir = self._config.base_dir
377
- pending = get_pending(base_dir, limit=20)
392
+ # Only drain items enqueued under THIS engine's profile — a queued
393
+ # memory must never materialize under a profile it was not written for.
394
+ pending = get_pending(base_dir, limit=20, profile_id=self.profile_id)
378
395
  if not pending:
379
396
  return
380
397
 
@@ -599,6 +616,7 @@ class MemoryEngine:
599
616
  *,
600
617
  include_global: bool | None = None,
601
618
  include_shared: bool | None = None,
619
+ window: str | tuple[str, str] | None = None,
602
620
  ) -> RecallResponse:
603
621
  """Recall relevant facts for a query.
604
622
 
@@ -609,10 +627,10 @@ class MemoryEngine:
609
627
  ``put_nowait`` and the actual ``pending_outcomes`` INSERT runs
610
628
  on a background worker.
611
629
 
612
- ``fast=True`` is the latency-bounded path: it skips spreading
613
- activation and remote agentic verification while retaining semantic,
614
- lexical, graph, temporal, and Hopfield retrieval. Use full recall when
615
- maximum multi-round quality matters more than response time.
630
+ ``fast=True`` is the latency-bounded path: it retains all six local
631
+ retrieval channels but skips remote agentic verification. Use
632
+ ``fast=False`` only when maximum multi-round quality matters more than
633
+ response time.
616
634
 
617
635
  Multi-scope: ``include_global`` / ``include_shared`` control which
618
636
  scopes participate in retrieval. ``None`` (the default) means "use the
@@ -650,6 +668,7 @@ class MemoryEngine:
650
668
  fast=fast,
651
669
  include_global=include_global,
652
670
  include_shared=include_shared,
671
+ window=window,
653
672
  )
654
673
  except Exception as exc:
655
674
  from superlocalmemory.infra.local_diagnostics import record_operation