superlocalmemory 3.6.23 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +271 -71
  3. package/bin/slm-npm +43 -89
  4. package/ide/configs/antigravity-mcp.json +2 -2
  5. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  6. package/ide/configs/claude-desktop-mcp.json +2 -2
  7. package/ide/configs/windsurf-mcp.json +2 -2
  8. package/ide/hooks/context-hook.js +6 -2
  9. package/ide/hooks/post-recall-hook.js +7 -3
  10. package/ide/hooks/tool-event-hook.sh +2 -1
  11. package/package.json +18 -10
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/_GENERATED.md +1 -1
  14. package/plugin/agents/slm-memory-advisor.md +1 -1
  15. package/plugin/requirements.txt +1 -1
  16. package/plugin/skills/slm-session/SKILL.md +1 -1
  17. package/plugin-src/rules/AGENTS.md +1 -1
  18. package/pyproject.toml +40 -8
  19. package/scripts/postinstall-interactive.js +17 -94
  20. package/scripts/postinstall.js +185 -258
  21. package/scripts/preuninstall.js +9 -50
  22. package/src/superlocalmemory/__init__.py +2 -2
  23. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  24. package/src/superlocalmemory/attribution/signer.py +34 -19
  25. package/src/superlocalmemory/attribution/watermark.py +1 -1
  26. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  27. package/src/superlocalmemory/cli/commands.py +453 -191
  28. package/src/superlocalmemory/cli/context_commands.py +5 -4
  29. package/src/superlocalmemory/cli/daemon.py +282 -187
  30. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  31. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  32. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  33. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  34. package/src/superlocalmemory/cli/main.py +128 -31
  35. package/src/superlocalmemory/cli/pending_store.py +54 -38
  36. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  37. package/src/superlocalmemory/cli/service_installer.py +57 -52
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  39. package/src/superlocalmemory/cli/version_banner.py +2 -1
  40. package/src/superlocalmemory/code_graph/config.py +3 -1
  41. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  42. package/src/superlocalmemory/core/config.py +65 -20
  43. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  44. package/src/superlocalmemory/core/context_cache.py +56 -8
  45. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  46. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  47. package/src/superlocalmemory/core/embeddings.py +54 -18
  48. package/src/superlocalmemory/core/engine.py +150 -104
  49. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  50. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  51. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  52. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  53. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  54. package/src/superlocalmemory/core/health_monitor.py +4 -2
  55. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  56. package/src/superlocalmemory/core/injection.py +69 -18
  57. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  58. package/src/superlocalmemory/core/maintenance.py +1 -1
  59. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  60. package/src/superlocalmemory/core/mutations.py +143 -0
  61. package/src/superlocalmemory/core/platform_utils.py +7 -4
  62. package/src/superlocalmemory/core/ram_lock.py +16 -5
  63. package/src/superlocalmemory/core/rate_limit.py +1 -1
  64. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  65. package/src/superlocalmemory/core/recall_worker.py +76 -59
  66. package/src/superlocalmemory/core/registry.py +1 -1
  67. package/src/superlocalmemory/core/scale_engine.py +293 -0
  68. package/src/superlocalmemory/core/score_contract.py +62 -0
  69. package/src/superlocalmemory/core/security_primitives.py +3 -1
  70. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  72. package/src/superlocalmemory/core/tier_manager.py +32 -20
  73. package/src/superlocalmemory/core/worker_pool.py +13 -4
  74. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  75. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  76. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  77. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  79. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  80. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  81. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  82. package/src/superlocalmemory/encoding/foresight.py +2 -0
  83. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  84. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  85. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  86. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  87. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  88. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  89. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  90. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  91. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  92. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  93. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  94. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  95. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  96. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  97. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  98. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  99. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  100. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  101. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  102. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  103. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  104. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  105. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  106. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  107. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  108. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  109. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  110. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  111. package/src/superlocalmemory/infra/backup.py +7 -5
  112. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  113. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  114. package/src/superlocalmemory/infra/data_root.py +199 -0
  115. package/src/superlocalmemory/infra/event_bus.py +3 -1
  116. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  117. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  118. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  119. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  120. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  121. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  122. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  123. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  124. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  125. package/src/superlocalmemory/learning/ensemble.py +11 -0
  126. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  127. package/src/superlocalmemory/learning/feedback.py +1 -1
  128. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  129. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  130. package/src/superlocalmemory/learning/ranker.py +4 -1
  131. package/src/superlocalmemory/learning/source_quality.py +1 -1
  132. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  133. package/src/superlocalmemory/llm/backbone.py +1 -1
  134. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  135. package/src/superlocalmemory/math/fisher.py +1 -1
  136. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  137. package/src/superlocalmemory/math/hopfield.py +1 -1
  138. package/src/superlocalmemory/math/langevin.py +1 -1
  139. package/src/superlocalmemory/math/polar_quant.py +3 -4
  140. package/src/superlocalmemory/math/qjl.py +1 -1
  141. package/src/superlocalmemory/math/sheaf.py +1 -1
  142. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  143. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  144. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  145. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  146. package/src/superlocalmemory/mcp/server.py +39 -13
  147. package/src/superlocalmemory/mcp/shared.py +69 -3
  148. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  149. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  150. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  151. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  152. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  153. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  154. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  155. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  156. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  157. package/src/superlocalmemory/mesh/broker.py +2 -0
  158. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  159. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  160. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  161. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  162. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  163. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  164. package/src/superlocalmemory/optimize/config/store.py +6 -4
  165. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  166. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  167. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  168. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  169. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  170. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  171. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  172. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  173. package/src/superlocalmemory/retrieval/engine.py +169 -79
  174. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  175. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  176. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  177. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  178. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  179. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  180. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  181. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  182. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  183. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  184. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  185. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  186. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  187. package/src/superlocalmemory/server/api.py +10 -7
  188. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  189. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  190. package/src/superlocalmemory/server/route_mutations.py +84 -0
  191. package/src/superlocalmemory/server/routes/agents.py +8 -6
  192. package/src/superlocalmemory/server/routes/brain.py +14 -12
  193. package/src/superlocalmemory/server/routes/chat.py +29 -12
  194. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  195. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  196. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  197. package/src/superlocalmemory/server/routes/memories.py +104 -43
  198. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  199. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  200. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  201. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  202. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  203. package/src/superlocalmemory/server/security_middleware.py +1 -1
  204. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  205. package/src/superlocalmemory/server/write_identity.py +147 -0
  206. package/src/superlocalmemory/storage/access_log.py +4 -3
  207. package/src/superlocalmemory/storage/database.py +118 -25
  208. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  209. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  210. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  211. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  212. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  213. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  214. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  215. package/src/superlocalmemory/storage/models.py +16 -0
  216. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  217. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  218. package/src/superlocalmemory/ui/favicon.svg +5 -0
  219. package/src/superlocalmemory/ui/index.html +1 -0
  220. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  221. package/src/superlocalmemory/ui/js/core.js +49 -8
  222. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  223. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  224. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  225. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  226. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  227. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  228. package/src/superlocalmemory/ui/js/settings.js +4 -2
  229. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  230. package/bin/slm +0 -59
  231. package/bin/slm.bat +0 -77
  232. package/bin/slm.cmd +0 -5
  233. package/ide/integrations/langchain/README.md +0 -106
  234. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  235. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  236. package/ide/integrations/langchain/pyproject.toml +0 -38
  237. package/ide/integrations/langchain/tests/__init__.py +0 -3
  238. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  239. package/ide/integrations/langchain/tests/test_security.py +0 -117
  240. package/ide/integrations/llamaindex/README.md +0 -81
  241. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  242. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  243. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  244. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  245. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  246. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  247. package/plugin-src/.mcp.json +0 -12
  248. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  249. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  250. package/plugin-src/hooks/.gitkeep +0 -0
  251. package/plugin-src/hooks/hooks.json +0 -23
  252. package/plugin-src/manifest.json +0 -25
  253. package/plugin-src/requirements.txt +0 -1
  254. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  255. package/plugin-src/scripts/ensure-venv.bat +0 -122
  256. package/plugin-src/scripts/ensure-venv.sh +0 -105
  257. package/plugin-src/scripts/slm-launch +0 -15
  258. package/plugin-src/scripts/slm-launch.bat +0 -17
  259. package/plugin-src/settings.json +0 -16
  260. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  261. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  262. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  263. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  264. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  265. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  266. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  267. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  268. package/scripts/_savings_math.py +0 -270
  269. package/scripts/build-dmg.sh +0 -417
  270. package/scripts/build-plugin.js +0 -742
  271. package/scripts/build-slm-hook.ps1 +0 -40
  272. package/scripts/build-slm-hook.sh +0 -45
  273. package/scripts/build_entry.py +0 -452
  274. package/scripts/ci/stage5b_gate.sh +0 -50
  275. package/scripts/dogfood_savings.py +0 -490
  276. package/scripts/generate-thumbnails.py +0 -218
  277. package/scripts/install-skills.ps1 +0 -4
  278. package/scripts/install-skills.sh +0 -5
  279. package/scripts/install.ps1 +0 -701
  280. package/scripts/install.sh +0 -1015
  281. package/scripts/postinstall_binary.js +0 -287
  282. package/scripts/prepack.js +0 -33
  283. package/scripts/release_manifest.py +0 -273
  284. package/scripts/slm-hook.spec +0 -56
  285. package/scripts/start-dashboard.ps1 +0 -52
  286. package/scripts/start-dashboard.sh +0 -41
  287. package/scripts/sync-wiki.ps1 +0 -127
  288. package/scripts/sync-wiki.sh +0 -82
  289. package/scripts/test-dmg.sh +0 -161
  290. package/scripts/test-npm-package.ps1 +0 -252
  291. package/scripts/test-npm-package.sh +0 -207
  292. package/scripts/verify-install.ps1 +0 -294
  293. package/scripts/verify-install.sh +0 -266
  294. package/scripts/verify-v27.ps1 +0 -301
  295. package/scripts/verify-v27.sh +0 -233
  296. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  297. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  298. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  299. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  300. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  301. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,246 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Capture derivation lineage without inventing source spans."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ from typing import Any
11
+
12
+
13
+ def _lineage_id(profile_id: str, object_type: str, object_id: str, operation_id: str) -> str:
14
+ value = f"{profile_id}\0{object_type}\0{object_id}\0{operation_id}"
15
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
16
+
17
+
18
+ def _table_exists(db: Any, table: str) -> bool:
19
+ return bool(db.execute(
20
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,),
21
+ ))
22
+
23
+
24
+ def _record(
25
+ db: Any,
26
+ *,
27
+ profile_id: str,
28
+ object_type: str,
29
+ object_id: str,
30
+ operation_id: str,
31
+ derivation_version: str,
32
+ source_status: str,
33
+ source_start: int | None = None,
34
+ source_end: int | None = None,
35
+ source_text_sha256: str = "",
36
+ source_fact_ids: tuple[str, ...] = (),
37
+ unresolved_reason: str = "",
38
+ ) -> None:
39
+ db.execute(
40
+ "INSERT OR REPLACE INTO derivation_lineage "
41
+ "(lineage_id,profile_id,object_type,object_id,operation_id,"
42
+ "derivation_version,source_status,source_start,source_end,"
43
+ "source_text_sha256,source_fact_ids_json,unresolved_reason) "
44
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
45
+ (
46
+ _lineage_id(profile_id, object_type, object_id, operation_id),
47
+ profile_id, object_type, object_id, operation_id,
48
+ derivation_version, source_status, source_start, source_end,
49
+ source_text_sha256,
50
+ json.dumps(list(source_fact_ids), separators=(",", ":")),
51
+ unresolved_reason,
52
+ ),
53
+ )
54
+
55
+
56
+ def _fact_ids(value: Any) -> tuple[str, ...]:
57
+ try:
58
+ decoded = json.loads(value or "[]")
59
+ except (TypeError, ValueError):
60
+ return ()
61
+ if not isinstance(decoded, list):
62
+ return ()
63
+ return tuple(str(item) for item in decoded)
64
+
65
+
66
+ def _capture_derived_rows(
67
+ db: Any,
68
+ *,
69
+ table: str,
70
+ id_column: str,
71
+ object_type: str,
72
+ profile_id: str,
73
+ operation_id: str,
74
+ derivation_version: str,
75
+ operation_fact_ids: frozenset[str],
76
+ ) -> None:
77
+ if not _table_exists(db, table):
78
+ return
79
+ rows = db.execute(
80
+ f'SELECT "{id_column}",fact_ids_json FROM "{table}" WHERE profile_id=?',
81
+ (profile_id,),
82
+ )
83
+ for row in rows:
84
+ source_ids = tuple(
85
+ fact_id
86
+ for fact_id in _fact_ids(row["fact_ids_json"])
87
+ if fact_id in operation_fact_ids
88
+ )
89
+ if not source_ids:
90
+ continue
91
+ _record(
92
+ db,
93
+ profile_id=profile_id,
94
+ object_type=object_type,
95
+ object_id=str(row[id_column]),
96
+ operation_id=operation_id,
97
+ derivation_version=derivation_version,
98
+ source_status="derived_from_facts",
99
+ source_fact_ids=source_ids,
100
+ unresolved_reason="direct_span_not_applicable",
101
+ )
102
+
103
+
104
+ def capture_operation_lineage(
105
+ db: Any,
106
+ *,
107
+ operation_id: str,
108
+ profile_id: str,
109
+ raw_content: str,
110
+ fact_ids: tuple[str, ...],
111
+ derivation_version: str,
112
+ ) -> None:
113
+ """Persist operation lineage while distinguishing spans from derivations."""
114
+ if not _table_exists(db, "derivation_lineage"):
115
+ return
116
+ fact_lineage: dict[str, dict[str, Any]] = {}
117
+ for fact_id in fact_ids:
118
+ rows = db.execute(
119
+ "SELECT content FROM atomic_facts WHERE fact_id=? AND profile_id=?",
120
+ (fact_id, profile_id),
121
+ )
122
+ if not rows:
123
+ _record(
124
+ db, profile_id=profile_id, object_type="fact", object_id=fact_id,
125
+ operation_id=operation_id, derivation_version=derivation_version,
126
+ source_status="unresolved", unresolved_reason="final_fact_missing",
127
+ )
128
+ fact_lineage[fact_id] = {
129
+ "source_status": "unresolved",
130
+ "unresolved_reason": "final_fact_missing",
131
+ }
132
+ continue
133
+ content = str(rows[0]["content"])
134
+ start = raw_content.find(content)
135
+ if start >= 0:
136
+ _record(
137
+ db, profile_id=profile_id, object_type="fact", object_id=fact_id,
138
+ operation_id=operation_id, derivation_version=derivation_version,
139
+ source_status="exact", source_start=start,
140
+ source_end=start + len(content),
141
+ source_text_sha256=hashlib.sha256(content.encode("utf-8")).hexdigest(),
142
+ )
143
+ fact_lineage[fact_id] = {
144
+ "source_status": "exact",
145
+ "source_start": start,
146
+ "source_end": start + len(content),
147
+ "source_text_sha256": hashlib.sha256(
148
+ content.encode("utf-8")
149
+ ).hexdigest(),
150
+ }
151
+ else:
152
+ _record(
153
+ db, profile_id=profile_id, object_type="fact", object_id=fact_id,
154
+ operation_id=operation_id, derivation_version=derivation_version,
155
+ source_status="unresolved",
156
+ unresolved_reason="no_exact_span_in_raw_source",
157
+ )
158
+ fact_lineage[fact_id] = {
159
+ "source_status": "unresolved",
160
+ "unresolved_reason": "no_exact_span_in_raw_source",
161
+ }
162
+
163
+ operation_fact_ids = frozenset(fact_ids)
164
+ _record(
165
+ db,
166
+ profile_id=profile_id,
167
+ object_type="profile",
168
+ object_id=profile_id,
169
+ operation_id=operation_id,
170
+ derivation_version=derivation_version,
171
+ source_status="not_applicable",
172
+ unresolved_reason="profile_scope_not_derived_from_source_span",
173
+ )
174
+ _capture_derived_rows(
175
+ db,
176
+ table="entity_profiles",
177
+ id_column="profile_entry_id",
178
+ object_type="entity_summary",
179
+ profile_id=profile_id,
180
+ operation_id=operation_id,
181
+ derivation_version=derivation_version,
182
+ operation_fact_ids=operation_fact_ids,
183
+ )
184
+ _capture_derived_rows(
185
+ db,
186
+ table="memory_scenes",
187
+ id_column="scene_id",
188
+ object_type="memory_scene",
189
+ profile_id=profile_id,
190
+ operation_id=operation_id,
191
+ derivation_version=derivation_version,
192
+ operation_fact_ids=operation_fact_ids,
193
+ )
194
+
195
+ if _table_exists(db, "graph_edges"):
196
+ rows = db.execute(
197
+ "SELECT edge_id,source_id,target_id FROM graph_edges WHERE profile_id=?",
198
+ (profile_id,),
199
+ )
200
+ for row in rows:
201
+ source_ids = tuple(
202
+ value
203
+ for value in (str(row["source_id"]), str(row["target_id"]))
204
+ if value in operation_fact_ids
205
+ )
206
+ if source_ids:
207
+ _record(
208
+ db,
209
+ profile_id=profile_id,
210
+ object_type="graph_edge",
211
+ object_id=str(row["edge_id"]),
212
+ operation_id=operation_id,
213
+ derivation_version=derivation_version,
214
+ source_status="derived_from_facts",
215
+ source_fact_ids=source_ids,
216
+ unresolved_reason="direct_span_not_applicable",
217
+ )
218
+
219
+ if _table_exists(db, "bm25_tokens"):
220
+ indexed_ids = {
221
+ str(row["fact_id"])
222
+ for row in db.execute(
223
+ "SELECT fact_id FROM bm25_tokens WHERE profile_id=?", (profile_id,)
224
+ )
225
+ }
226
+ for fact_id in operation_fact_ids & indexed_ids:
227
+ lineage = fact_lineage.get(fact_id, {})
228
+ _record(
229
+ db,
230
+ profile_id=profile_id,
231
+ object_type="index_bm25",
232
+ object_id=fact_id,
233
+ operation_id=operation_id,
234
+ derivation_version=derivation_version,
235
+ source_status=str(lineage.get("source_status", "unresolved")),
236
+ source_start=lineage.get("source_start"),
237
+ source_end=lineage.get("source_end"),
238
+ source_text_sha256=str(lineage.get("source_text_sha256", "")),
239
+ source_fact_ids=(fact_id,),
240
+ unresolved_reason=str(
241
+ lineage.get("unresolved_reason", "source_fact_lineage_missing")
242
+ ),
243
+ )
244
+
245
+
246
+ __all__ = ["capture_operation_lineage"]
@@ -24,6 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import json
26
26
  import os
27
+ import platform
27
28
  import signal
28
29
  import sys
29
30
 
@@ -52,6 +53,18 @@ def _start_parent_watchdog() -> None:
52
53
  start_parent_watchdog()
53
54
 
54
55
 
56
+ def _embedding_backend_order() -> tuple[str, str]:
57
+ """Return the stable local backend preference for this platform.
58
+
59
+ ONNX model export/loading can stall indefinitely on Apple Silicon while
60
+ the same cached model loads through PyTorch CPU in a few seconds. Keep
61
+ ONNX first elsewhere and retain it as a fallback on macOS.
62
+ """
63
+ if sys.platform == "darwin" and platform.machine().lower() == "arm64":
64
+ return ("pytorch", "onnx")
65
+ return ("onnx", "pytorch")
66
+
67
+
55
68
  def _load_embedding_model(name: str) -> tuple:
56
69
  """Load embedding model. ONNX CPU-only first, PyTorch fallback.
57
70
 
@@ -59,26 +72,25 @@ def _load_embedding_model(name: str) -> tuple:
59
72
  """
60
73
  from sentence_transformers import SentenceTransformer
61
74
 
62
- # ONNX with explicit CPU provider — avoids CoreML EP memory overhead.
63
- try:
64
- m = SentenceTransformer(
65
- name,
66
- backend="onnx",
67
- trust_remote_code=True,
68
- model_kwargs={"provider": "CPUExecutionProvider"},
69
- )
70
- return m, "onnx"
71
- except Exception:
72
- pass
73
-
74
- # PyTorch CPU fallback.
75
- try:
76
- import torch
77
- with torch.inference_mode():
78
- m = SentenceTransformer(name, trust_remote_code=True, device="cpu")
79
- return m, "pytorch"
80
- except Exception:
81
- return None, ""
75
+ for backend in _embedding_backend_order():
76
+ try:
77
+ if backend == "onnx":
78
+ m = SentenceTransformer(
79
+ name,
80
+ backend="onnx",
81
+ trust_remote_code=True,
82
+ model_kwargs={"provider": "CPUExecutionProvider"},
83
+ )
84
+ else:
85
+ import torch
86
+ with torch.inference_mode():
87
+ m = SentenceTransformer(
88
+ name, trust_remote_code=True, device="cpu",
89
+ )
90
+ return m, backend
91
+ except Exception:
92
+ continue
93
+ return None, ""
82
94
 
83
95
 
84
96
  def _worker_main() -> None:
@@ -61,12 +61,22 @@ class DimensionMismatchError(RuntimeError):
61
61
  # This lock is the secondary safety net for when the daemon isn't available.
62
62
  # ---------------------------------------------------------------------------
63
63
 
64
- _EMBEDDING_LOCK_FILE = Path.home() / ".superlocalmemory" / ".embedding.lock"
65
- _EMBEDDING_PID_FILE = Path.home() / ".superlocalmemory" / ".embedding-worker.pid"
66
64
  _MAX_CONCURRENT_WORKERS = int(os.environ.get("SLM_MAX_EMBEDDING_WORKERS", 1))
67
65
  _embedding_lock_fd: int | None = None
68
66
 
69
67
 
68
+ def _embedding_lock_file() -> Path:
69
+ from superlocalmemory.infra.data_root import state_path
70
+
71
+ return state_path(".embedding.lock")
72
+
73
+
74
+ def _embedding_pid_file() -> Path:
75
+ from superlocalmemory.infra.data_root import state_path
76
+
77
+ return state_path(".embedding-worker.pid")
78
+
79
+
70
80
  def _is_embedding_worker_alive() -> bool:
71
81
  """Check if an embedding worker PID file exists and that PID is alive.
72
82
 
@@ -74,21 +84,23 @@ def _is_embedding_worker_alive() -> bool:
74
84
  check if one is already running. Prevents duplicate 1.6GB workers.
75
85
  """
76
86
  try:
77
- if not _EMBEDDING_PID_FILE.exists():
87
+ pid_file = _embedding_pid_file()
88
+ if not pid_file.exists():
78
89
  return False
79
- pid = int(_EMBEDDING_PID_FILE.read_text().strip())
90
+ pid = int(pid_file.read_text().strip())
80
91
  os.kill(pid, 0) # Signal 0 = check if alive
81
92
  return True
82
93
  except (ValueError, OSError, ProcessLookupError):
83
94
  # PID file invalid or process dead — clean up stale file
84
- _EMBEDDING_PID_FILE.unlink(missing_ok=True)
95
+ _embedding_pid_file().unlink(missing_ok=True)
85
96
  return False
86
97
 
87
98
 
88
99
  def register_embedding_worker_pid(pid: int) -> None:
89
100
  """Write the embedding worker PID to the machine-wide PID file."""
90
- _EMBEDDING_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
91
- _EMBEDDING_PID_FILE.write_text(str(pid))
101
+ pid_file = _embedding_pid_file()
102
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
103
+ pid_file.write_text(str(pid))
92
104
 
93
105
 
94
106
  def acquire_embedding_lock(timeout: float = 5.0) -> bool:
@@ -108,10 +120,11 @@ def acquire_embedding_lock(timeout: float = 5.0) -> bool:
108
120
  return True # No file locking on Windows — PID check above is the guard
109
121
 
110
122
  import fcntl
111
- _EMBEDDING_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
123
+ lock_file = _embedding_lock_file()
124
+ lock_file.parent.mkdir(parents=True, exist_ok=True)
112
125
 
113
126
  try:
114
- _embedding_lock_fd = os.open(str(_EMBEDDING_LOCK_FILE), os.O_CREAT | os.O_RDWR)
127
+ _embedding_lock_fd = os.open(str(lock_file), os.O_CREAT | os.O_RDWR)
115
128
  deadline = time.time() + timeout
116
129
  while time.time() < deadline:
117
130
  try:
@@ -505,22 +518,45 @@ class EmbeddingService:
505
518
  self._worker_proc = None
506
519
 
507
520
  def _kill_worker(self) -> None:
508
- """Terminate worker subprocess."""
521
+ """Terminate the worker and close every owned pipe exactly once."""
509
522
  if self._idle_timer is not None:
510
523
  self._idle_timer.cancel()
511
524
  self._idle_timer = None
512
- if self._worker_proc is not None:
525
+
526
+ proc = self._worker_proc
527
+ if proc is not None:
528
+ # Detach first so re-entrant/finalizer cleanup is idempotent.
529
+ self._worker_proc = None
530
+ self._worker_ready = False
513
531
  try:
514
- self._worker_proc.stdin.write('{"cmd":"quit"}\n')
515
- self._worker_proc.stdin.flush()
516
- self._worker_proc.wait(timeout=3)
532
+ proc.stdin.write('{"cmd":"quit"}\n')
533
+ proc.stdin.flush()
534
+ proc.wait(timeout=3)
517
535
  except Exception:
518
536
  try:
519
- self._worker_proc.kill()
537
+ returncode = proc.poll()
520
538
  except Exception:
521
- pass
522
- self._worker_proc = None
523
- self._worker_ready = False
539
+ returncode = None
540
+ # MagicMock/unknown poll results are treated conservatively as
541
+ # live; a real exited child always reports an integer code.
542
+ if returncode is None or not isinstance(returncode, int):
543
+ try:
544
+ proc.kill()
545
+ proc.wait(timeout=3)
546
+ except Exception:
547
+ pass
548
+ finally:
549
+ # TextIOWrapper.close() can itself raise BrokenPipeError while
550
+ # flushing buffered stdin. Suppress it here, while the stream
551
+ # is still strongly referenced, so it cannot surface later as
552
+ # an unraisable finalizer warning.
553
+ for stream_name in ("stdin", "stdout", "stderr"):
554
+ stream = getattr(proc, stream_name, None)
555
+ if stream is not None:
556
+ try:
557
+ stream.close()
558
+ except (BrokenPipeError, OSError, ValueError):
559
+ pass
524
560
 
525
561
  def _reset_idle_timer(self) -> None:
526
562
  """Reset idle timer — kills worker after 2 min inactivity."""