superlocalmemory 3.6.22 → 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 (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. 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."""