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,147 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later
3
+
4
+ """Shared HTTP write-identity boundary.
5
+
6
+ Caller-selected IDE or agent labels are audit metadata. Authorization is
7
+ derived only from the private capability for the exact daemon instance or the
8
+ local install token used by the same-origin dashboard.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import hmac
15
+ import os
16
+ from typing import Any
17
+
18
+ from fastapi import HTTPException, Request
19
+
20
+
21
+ def _header(request: Request, name: str) -> str:
22
+ headers = request.headers
23
+ value = headers.get(name, "")
24
+ if value:
25
+ return str(value)
26
+ lowered = name.lower()
27
+ for key, candidate in headers.items():
28
+ if str(key).lower() == lowered:
29
+ return str(candidate)
30
+ return ""
31
+
32
+
33
+ def require_daemon_actor(request: Request, descriptor: Any | None) -> str:
34
+ """Require the private capability for one exact daemon instance."""
35
+ capability = _header(request, "X-SLM-Daemon-Capability")
36
+ if descriptor is None or not hmac.compare_digest(
37
+ capability,
38
+ str(descriptor.capability),
39
+ ):
40
+ raise HTTPException(403, detail="Invalid daemon capability")
41
+ target_instance = _header(request, "X-SLM-Target-Instance")
42
+ if not hmac.compare_digest(
43
+ target_instance,
44
+ str(descriptor.instance_id),
45
+ ):
46
+ raise HTTPException(409, detail="Daemon instance changed")
47
+ return f"daemon-capability:{descriptor.capability_fingerprint}"
48
+
49
+
50
+ def require_write_actor(
51
+ request: Request,
52
+ descriptor: Any | None,
53
+ *,
54
+ actor_kind: str = "dashboard",
55
+ ) -> str:
56
+ """Return a capability-derived actor or reject the write."""
57
+ if _header(request, "X-SLM-Daemon-Capability"):
58
+ return require_daemon_actor(request, descriptor)
59
+
60
+ from superlocalmemory.core.security_primitives import verify_install_token
61
+
62
+ if verify_install_token(_header(request, "X-Install-Token")):
63
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
64
+
65
+ return local_trusted_actor_id(actor_kind)
66
+
67
+ from superlocalmemory.infra.auth_middleware import verify_api_key
68
+
69
+ api_key = _header(request, "X-SLM-API-Key")
70
+ if verify_api_key(api_key):
71
+ fingerprint = hashlib.sha256(
72
+ b"superlocalmemory-api-actor-v1\0" + api_key.encode("utf-8")
73
+ ).hexdigest()
74
+ return f"api-key:{actor_kind}:{fingerprint}"
75
+
76
+ raise HTTPException(403, detail="Authenticated write capability required")
77
+
78
+
79
+ def require_http_mutation_actor(
80
+ request: Request,
81
+ descriptor: Any | None,
82
+ *,
83
+ actor_kind: str = "http-route",
84
+ mesh_secret: str | None = None,
85
+ ) -> str:
86
+ """Derive a principal for any state-changing HTTP operation.
87
+
88
+ Private credentials always take precedence. An uncredentialed loopback
89
+ peer is the same local-user boundary as the filesystem capability and gets
90
+ a derived local actor. Non-loopback callers fail closed. Mesh credentials
91
+ are accepted only when the broker has an explicit shared secret.
92
+ """
93
+ credential_headers = (
94
+ "X-SLM-Daemon-Capability",
95
+ "X-Install-Token",
96
+ "X-SLM-API-Key",
97
+ )
98
+ if any(_header(request, name) for name in credential_headers):
99
+ return require_write_actor(
100
+ request,
101
+ descriptor,
102
+ actor_kind=actor_kind,
103
+ )
104
+
105
+ client_host = request.client.host if request.client else ""
106
+ is_test_client = (
107
+ client_host == "testclient"
108
+ and os.environ.get("SLM_TEST_ISOLATION") == "1"
109
+ )
110
+ if client_host in ("127.0.0.1", "::1", "localhost") or is_test_client:
111
+ from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
112
+
113
+ return local_trusted_actor_id(actor_kind)
114
+
115
+ if mesh_secret:
116
+ presented = (
117
+ _header(request, "X-Mesh-Secret")
118
+ or _header(request, "Authorization").removeprefix("Bearer ").strip()
119
+ )
120
+ if presented and hmac.compare_digest(presented, mesh_secret):
121
+ fingerprint = hashlib.sha256(
122
+ b"superlocalmemory-mesh-actor-v1\0" + presented.encode("utf-8")
123
+ ).hexdigest()
124
+ return f"mesh-secret:{fingerprint}"
125
+
126
+ raise HTTPException(403, detail="Authenticated mutation capability required")
127
+
128
+
129
+ def authenticated_request_actor(
130
+ request: Request,
131
+ descriptor: Any | None = None,
132
+ *,
133
+ actor_kind: str = "http-route",
134
+ ) -> str:
135
+ """Return the middleware-verified principal or verify route credentials."""
136
+ actor = getattr(request.state, "authenticated_actor", "")
137
+ if actor:
138
+ return str(actor)
139
+ return require_write_actor(request, descriptor, actor_kind=actor_kind)
140
+
141
+
142
+ __all__ = [
143
+ "require_daemon_actor",
144
+ "authenticated_request_actor",
145
+ "require_http_mutation_actor",
146
+ "require_write_actor",
147
+ ]
@@ -2,14 +2,15 @@
2
2
  # Licensed under AGPL-3.0-or-later - see LICENSE file
3
3
  # Part of SuperLocalMemory V3
4
4
 
5
- """Access log for fact retrieval events.
5
+ """Exposure log for fact retrieval events.
6
6
 
7
7
  Tracks when facts are accessed (recall, auto_invoke, search).
8
- Used by Phase 2 auto-invoke for recency scoring (H1 fix).
8
+ The historical table name is retained for compatibility. A ``recall`` row is
9
+ exposure telemetry only and must never be interpreted as positive feedback.
9
10
  All SQL parameterized (Rule 11). Silent errors (Rule 19).
10
11
 
11
12
  Part of Qualixar | Author: Varun Pratap Bhardwaj
12
- License: Elastic-2.0
13
+ License: AGPL-3.0-or-later
13
14
  """
14
15
 
15
16
  from __future__ import annotations
@@ -130,7 +130,12 @@ class DatabaseManager:
130
130
  self.db_path = Path(db_path)
131
131
  self.db_path.parent.mkdir(parents=True, exist_ok=True)
132
132
  self._lock = threading.Lock()
133
- self._txn_conn: sqlite3.Connection | None = None
133
+ # Transaction connections are thread-affine in sqlite3. A manager is
134
+ # shared across HTTP, materializer, and worker threads, so a process-
135
+ # global connection slot lets another thread accidentally execute on
136
+ # an uncommitted foreign connection. Keep the active connection local
137
+ # to the thread that owns the transaction.
138
+ self._txn_state = threading.local()
134
139
  self._enable_wal()
135
140
 
136
141
  def _enable_wal(self) -> None:
@@ -174,7 +179,7 @@ class DatabaseManager:
174
179
  """Atomic transaction. All writes commit or rollback together."""
175
180
  with self._lock:
176
181
  conn = self._connect()
177
- self._txn_conn = conn
182
+ self._txn_state.conn = conn
178
183
  try:
179
184
  yield
180
185
  conn.commit()
@@ -182,7 +187,7 @@ class DatabaseManager:
182
187
  conn.rollback()
183
188
  raise
184
189
  finally:
185
- self._txn_conn = None
190
+ self._txn_state.conn = None
186
191
  conn.close()
187
192
 
188
193
  @contextmanager
@@ -196,7 +201,7 @@ class DatabaseManager:
196
201
  """
197
202
  with self._lock:
198
203
  conn = self._connect()
199
- self._txn_conn = conn
204
+ self._txn_state.conn = conn
200
205
  try:
201
206
  yield conn
202
207
  conn.commit()
@@ -204,7 +209,7 @@ class DatabaseManager:
204
209
  conn.rollback()
205
210
  raise
206
211
  finally:
207
- self._txn_conn = None
212
+ self._txn_state.conn = None
208
213
  conn.close()
209
214
 
210
215
  def execute(self, sql: str, params: tuple[Any, ...] = ()) -> list[sqlite3.Row]:
@@ -212,8 +217,9 @@ class DatabaseManager:
212
217
 
213
218
  Uses shared conn inside transaction, else per-call with retry.
214
219
  """
215
- if self._txn_conn is not None:
216
- return self._txn_conn.execute(sql, params).fetchall()
220
+ transaction_conn = getattr(self._txn_state, "conn", None)
221
+ if transaction_conn is not None:
222
+ return transaction_conn.execute(sql, params).fetchall()
217
223
 
218
224
  last_error: Exception | None = None
219
225
  for attempt in range(_MAX_RETRIES):
@@ -438,6 +444,35 @@ class DatabaseManager:
438
444
  )
439
445
  return [self._row_to_fact(r) for r in rows]
440
446
 
447
+ def get_external_visible_facts(
448
+ self,
449
+ profile_id: str,
450
+ *,
451
+ include_global: bool = False,
452
+ include_shared: bool = False,
453
+ ) -> list[AtomicFact]:
454
+ """Cross-profile facts visible to ``profile_id`` under scope policy.
455
+
456
+ This is the bounded supplement used by profile-partitioned candidate
457
+ indexes. It deliberately excludes the requester's own partition so a
458
+ fast local index can merge only the global/authorized-shared rows it
459
+ cannot discover itself. The canonical scope predicate remains the
460
+ sole authorization rule.
461
+ """
462
+ if not include_global and not include_shared:
463
+ return []
464
+ where, params = _scope_where(
465
+ profile_id,
466
+ include_global=include_global,
467
+ include_shared=include_shared,
468
+ )
469
+ rows = self.execute(
470
+ f"SELECT * FROM atomic_facts WHERE {where} AND profile_id != ? "
471
+ "ORDER BY created_at DESC",
472
+ (*params, profile_id),
473
+ )
474
+ return [self._row_to_fact(r) for r in rows]
475
+
441
476
  _MAX_FACTS_PER_ENTITY_LOOKUP: int = 100
442
477
 
443
478
  def get_facts_by_entity(
@@ -755,11 +790,35 @@ class DatabaseManager:
755
790
  (fact_id, profile_id, json.dumps(tokens)),
756
791
  )
757
792
 
758
- def get_all_bm25_tokens(self, profile_id: str) -> dict[str, list[str]]:
759
- """Load full BM25 index: fact_id -> token list."""
793
+ def get_all_bm25_tokens(
794
+ self,
795
+ profile_id: str,
796
+ include_global: bool = False,
797
+ include_shared: bool = False,
798
+ ) -> dict[str, list[str]]:
799
+ """Load the visible legacy BM25 index: fact_id -> token list."""
800
+ if not include_global and not include_shared:
801
+ # Preserve the historical token-store contract, including repair
802
+ # tooling that can inspect orphaned token rows before facts exist.
803
+ rows = self.execute(
804
+ "SELECT fact_id, tokens FROM bm25_tokens WHERE profile_id = ?",
805
+ (profile_id,),
806
+ )
807
+ return {
808
+ dict(row)["fact_id"]: json.loads(dict(row)["tokens"])
809
+ for row in rows
810
+ }
811
+ where, params = _scope_where(
812
+ profile_id,
813
+ include_global=include_global,
814
+ include_shared=include_shared,
815
+ prefix="af",
816
+ )
760
817
  rows = self.execute(
761
- "SELECT fact_id, tokens FROM bm25_tokens WHERE profile_id = ?",
762
- (profile_id,),
818
+ "SELECT bt.fact_id, bt.tokens FROM bm25_tokens AS bt "
819
+ "JOIN atomic_facts AS af ON af.fact_id = bt.fact_id "
820
+ f"WHERE {where}",
821
+ (*params,),
763
822
  )
764
823
  return {dict(r)["fact_id"]: json.loads(dict(r)["tokens"]) for r in rows}
765
824
 
@@ -1383,6 +1442,36 @@ class DatabaseManager:
1383
1442
  Retries 3x on SQLITE_BUSY (handled by execute()).
1384
1443
  All SQL parameterized (HR-05).
1385
1444
  """
1445
+ from superlocalmemory.core.lifecycle_state import atomic_lifecycle_for
1446
+
1447
+ with self.transaction():
1448
+ self._upsert_retention_in_transaction(
1449
+ fact_id=fact_id,
1450
+ profile_id=profile_id,
1451
+ retention_score=retention_score,
1452
+ memory_strength=memory_strength,
1453
+ access_count=access_count,
1454
+ last_accessed_at=last_accessed_at,
1455
+ lifecycle_zone=lifecycle_zone,
1456
+ )
1457
+ self.execute(
1458
+ "UPDATE atomic_facts SET lifecycle = ? "
1459
+ "WHERE fact_id = ? AND profile_id = ?",
1460
+ (atomic_lifecycle_for(lifecycle_zone), fact_id, profile_id),
1461
+ )
1462
+
1463
+ def _upsert_retention_in_transaction(
1464
+ self,
1465
+ *,
1466
+ fact_id: str,
1467
+ profile_id: str,
1468
+ retention_score: float,
1469
+ memory_strength: float,
1470
+ access_count: int,
1471
+ last_accessed_at: str,
1472
+ lifecycle_zone: str,
1473
+ ) -> None:
1474
+ """Write one retention row using the caller's active transaction."""
1386
1475
  self.execute(
1387
1476
  "INSERT INTO fact_retention "
1388
1477
  "(fact_id, profile_id, retention_score, memory_strength, "
@@ -1409,9 +1498,11 @@ class DatabaseManager:
1409
1498
  Returns count of successfully upserted rows.
1410
1499
  """
1411
1500
  count = 0
1501
+ from superlocalmemory.core.lifecycle_state import atomic_lifecycle_for
1502
+
1412
1503
  with self.transaction():
1413
1504
  for f in facts:
1414
- self.upsert_retention(
1505
+ self._upsert_retention_in_transaction(
1415
1506
  fact_id=f["fact_id"],
1416
1507
  profile_id=profile_id,
1417
1508
  retention_score=f["retention"],
@@ -1420,6 +1511,11 @@ class DatabaseManager:
1420
1511
  last_accessed_at=f["last_accessed_at"],
1421
1512
  lifecycle_zone=f["zone"],
1422
1513
  )
1514
+ self.execute(
1515
+ "UPDATE atomic_facts SET lifecycle = ? "
1516
+ "WHERE fact_id = ? AND profile_id = ?",
1517
+ (atomic_lifecycle_for(f["zone"]), f["fact_id"], profile_id),
1518
+ )
1423
1519
  count += 1
1424
1520
  return count
1425
1521
 
@@ -1464,20 +1560,17 @@ class DatabaseManager:
1464
1560
  )
1465
1561
  return
1466
1562
 
1467
- # Update fact_retention
1468
- self.execute(
1469
- "UPDATE fact_retention SET lifecycle_zone = 'forgotten', "
1470
- " retention_score = 0.0 "
1471
- "WHERE fact_id = ? AND profile_id = ?",
1472
- (fact_id, profile_id),
1473
- )
1563
+ from superlocalmemory.core.lifecycle_state import set_fact_lifecycle_zone
1474
1564
 
1475
- # Mark in atomic_facts as archived (valid enum value per A-CRIT-01)
1476
- self.execute(
1477
- "UPDATE atomic_facts SET lifecycle = 'archived' "
1478
- "WHERE fact_id = ? AND profile_id = ?",
1479
- (fact_id, profile_id),
1480
- )
1565
+ with self.transaction():
1566
+ set_fact_lifecycle_zone(
1567
+ self, [fact_id], "forgotten", profile_id=profile_id,
1568
+ )
1569
+ self.execute(
1570
+ "UPDATE fact_retention SET retention_score = 0.0 "
1571
+ "WHERE fact_id = ? AND profile_id = ?",
1572
+ (fact_id, profile_id),
1573
+ )
1481
1574
 
1482
1575
  # ------------------------------------------------------------------
1483
1576
  # Phase E: CCQ Consolidated Blocks & Audit CRUD
@@ -34,25 +34,64 @@ import sqlite3
34
34
  from dataclasses import dataclass, field
35
35
  from datetime import datetime, timezone
36
36
  from pathlib import Path
37
- from typing import Iterable
38
37
 
39
38
  from superlocalmemory.storage.migrations import (
40
39
  M001_add_signal_features_columns as _M001,
40
+ )
41
+ from superlocalmemory.storage.migrations import (
41
42
  M002_model_state_history as _M002,
43
+ )
44
+ from superlocalmemory.storage.migrations import (
42
45
  M003_migration_log as _M003,
46
+ )
47
+ from superlocalmemory.storage.migrations import (
43
48
  M004_cross_platform_sync_log as _M004,
49
+ )
50
+ from superlocalmemory.storage.migrations import (
44
51
  M005_bandit_tables as _M005,
52
+ )
53
+ from superlocalmemory.storage.migrations import (
45
54
  M006_action_outcomes_reward as _M006,
55
+ )
56
+ from superlocalmemory.storage.migrations import (
46
57
  M007_pending_outcomes as _M007,
58
+ )
59
+ from superlocalmemory.storage.migrations import (
47
60
  M009_model_lineage as _M009,
61
+ )
62
+ from superlocalmemory.storage.migrations import (
48
63
  M010_evolution_config as _M010,
64
+ )
65
+ from superlocalmemory.storage.migrations import (
49
66
  M011_archive_and_merge as _M011,
67
+ )
68
+ from superlocalmemory.storage.migrations import (
50
69
  M012_shadow_observations as _M012,
70
+ )
71
+ from superlocalmemory.storage.migrations import (
51
72
  M013_bi_temporal_columns as _M013,
73
+ )
74
+ from superlocalmemory.storage.migrations import (
52
75
  M014_v345_scale_ready as _M014,
76
+ )
77
+ from superlocalmemory.storage.migrations import (
53
78
  M015_add_pinned_column as _M015,
79
+ )
80
+ from superlocalmemory.storage.migrations import (
54
81
  M016_add_scope_support as _M016,
55
82
  )
83
+ from superlocalmemory.storage.migrations import (
84
+ M017_ccq_scope_column as _M017,
85
+ )
86
+ from superlocalmemory.storage.migrations import (
87
+ M018_ingestion_operations as _M018,
88
+ )
89
+ from superlocalmemory.storage.migrations import (
90
+ M019_derivation_lineage as _M019,
91
+ )
92
+ from superlocalmemory.storage.migrations import (
93
+ M020_model_state_integrity as _M020,
94
+ )
56
95
 
57
96
  # Map migration name → module (used for the optional ``verify(conn)`` hook
58
97
  # that lets the runner detect "already applied" state when an idempotent
@@ -73,6 +112,10 @@ _MODULES = {
73
112
  _M014.NAME: _M014,
74
113
  _M015.NAME: _M015,
75
114
  _M016.NAME: _M016,
115
+ _M017.NAME: _M017,
116
+ _M018.NAME: _M018,
117
+ _M019.NAME: _M019,
118
+ _M020.NAME: _M020,
76
119
  }
77
120
 
78
121
  logger = logging.getLogger(__name__)
@@ -101,6 +144,10 @@ MIGRATIONS: list[Migration] = [
101
144
  # M009 extends learning_model_state (created by M002).
102
145
  Migration(name=_M009.NAME, db_target="learning", ddl=_M009.DDL,
103
146
  dependencies=(_M002.NAME,)),
147
+ # M020 owns post-release integrity repair. M002 remains byte-for-byte
148
+ # compatible with databases that recorded its historical DDL hash.
149
+ Migration(name=_M020.NAME, db_target="learning", ddl=_M020.DDL,
150
+ dependencies=(_M002.NAME,)),
104
151
  # M010 creates evolution_config + evolution_llm_cost_log (learning.db).
105
152
  Migration(name=_M010.NAME, db_target="learning", ddl=_M010.DDL,
106
153
  dependencies=(_M003.NAME,)),
@@ -111,6 +158,10 @@ MIGRATIONS: list[Migration] = [
111
158
  Migration(name=_M004.NAME, db_target="memory", ddl=_M004.DDL),
112
159
  # M007 creates pending_outcomes (memory.db, LLD-00 §1.2).
113
160
  Migration(name=_M007.NAME, db_target="memory", ddl=_M007.DDL),
161
+ # M018 is additive and independent of runtime-bootstrapped tables.
162
+ Migration(name=_M018.NAME, db_target="memory", ddl=_M018.DDL),
163
+ Migration(name=_M019.NAME, db_target="memory", ddl=_M019.DDL,
164
+ dependencies=(_M018.NAME,)),
114
165
  # M006 + M011 are deliberately NOT here — see DEFERRED_MIGRATIONS below.
115
166
  ]
116
167
 
@@ -139,6 +190,8 @@ DEFERRED_MIGRATIONS: list[Migration] = [
139
190
  # M016 adds scope and shared_with columns to 5 core tables for
140
191
  # multi-scope memory support (personal/global/shared).
141
192
  Migration(name=_M016.NAME, db_target="memory", ddl=_M016.DDL),
193
+ # M017 adds scope to the engine-bootstrapped CCQ consolidation table.
194
+ Migration(name=_M017.NAME, db_target="memory", ddl=_M017.DDL),
142
195
  ]
143
196
 
144
197
 
@@ -395,6 +448,25 @@ def _bootstrap_both_migration_logs(
395
448
  return failed, details
396
449
 
397
450
 
451
+ def _bootstrap_learning_schema(learning_db: Path, *, dry_run: bool) -> str | None:
452
+ """Create the base learning tables before forward migrations extend them.
453
+
454
+ ``apply_all`` is called by the daemon before ``MemoryEngine`` exists. A
455
+ blank first-install therefore has no ``learning_signals`` or
456
+ ``learning_model_state`` tables for M001/M002/M009 to alter. The runner
457
+ owns this prerequisite so every caller has the same first-boot contract.
458
+ """
459
+ if dry_run:
460
+ return None
461
+ try:
462
+ from superlocalmemory.learning.database import LearningDatabase
463
+
464
+ LearningDatabase(learning_db)
465
+ except Exception as exc: # noqa: BLE001 - retain runner's non-fatal API
466
+ return f"learning schema bootstrap failed: {type(exc).__name__}: {exc}"
467
+ return None
468
+
469
+
398
470
  def apply_all(
399
471
  learning_db: Path,
400
472
  memory_db: Path,
@@ -411,6 +483,17 @@ def apply_all(
411
483
  failed: list[str] = []
412
484
  details: dict[str, str] = {}
413
485
 
486
+ schema_error = _bootstrap_learning_schema(learning_db, dry_run=dry_run)
487
+ if schema_error is not None:
488
+ failed.append("learning_schema_bootstrap")
489
+ details["learning_schema_bootstrap"] = schema_error
490
+ return {
491
+ "applied": applied,
492
+ "skipped": skipped,
493
+ "failed": failed,
494
+ "details": details,
495
+ }
496
+
414
497
  # S9-W1 C3: unify the migration_log bootstrap across both DBs up-front.
415
498
  bs_failed, bs_details = _bootstrap_both_migration_logs(
416
499
  learning_db, memory_db, dry_run=dry_run,
@@ -21,7 +21,7 @@ by create_all_tables(). This migration module detects their absence
21
21
  and creates them for databases that were created before SLM 3.3.
22
22
 
23
23
  Part of Qualixar | Author: Varun Pratap Bhardwaj
24
- License: Elastic-2.0
24
+ License: AGPL-3.0-or-later
25
25
  """
26
26
 
27
27
  from __future__ import annotations
@@ -16,7 +16,6 @@ reaching the LightGBM deserialiser.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- import hashlib
20
19
  import sqlite3
21
20
 
22
21
  NAME = "M002_model_state_history"
@@ -40,19 +39,16 @@ def verify(conn: sqlite3.Connection) -> bool:
40
39
  return _REQUIRED_COLS <= cols
41
40
 
42
41
 
43
- # S9-W1 M-DATA-01: prior version of this migration hardcoded ``is_active=1``
44
- # for every copied row. v3.4.19 mainline has ``UNIQUE(profile_id)`` which
45
- # guarantees one row per profile so the hardcode worked — but a non-mainline
46
- # dev-build could have multiple rows and the partial unique index created
47
- # after rebuild would fail transactionally. We now mark only the row with
48
- # the MAX(id) per profile as active; everything else becomes history.
42
+ # IMPORTANT: this DDL shipped in V3.4.21. Migration hashes are immutable
43
+ # upgrade contracts, so later improvements belong in a new forward migration
44
+ # (M020), never in this historical definition.
49
45
  DDL = """
50
46
  BEGIN IMMEDIATE;
51
47
 
52
48
  CREATE TABLE learning_model_state_new (
53
49
  id INTEGER PRIMARY KEY AUTOINCREMENT,
54
50
  profile_id TEXT NOT NULL,
55
- model_version TEXT NOT NULL DEFAULT '3.4.22',
51
+ model_version TEXT NOT NULL DEFAULT '3.4.21',
56
52
  state_bytes BLOB NOT NULL,
57
53
  bytes_sha256 TEXT NOT NULL DEFAULT '',
58
54
  trained_on_count INTEGER NOT NULL DEFAULT 0,
@@ -65,14 +61,8 @@ CREATE TABLE learning_model_state_new (
65
61
 
66
62
  INSERT INTO learning_model_state_new
67
63
  (profile_id, state_bytes, is_active, trained_at, updated_at)
68
- SELECT lms.profile_id, lms.state_bytes,
69
- CASE WHEN lms.id = (
70
- SELECT MAX(lms2.id)
71
- FROM learning_model_state lms2
72
- WHERE lms2.profile_id = lms.profile_id
73
- ) THEN 1 ELSE 0 END,
74
- lms.updated_at, lms.updated_at
75
- FROM learning_model_state lms;
64
+ SELECT profile_id, state_bytes, 1, updated_at, updated_at
65
+ FROM learning_model_state;
76
66
 
77
67
  DROP TABLE learning_model_state;
78
68
  ALTER TABLE learning_model_state_new RENAME TO learning_model_state;
@@ -86,47 +76,3 @@ CREATE INDEX idx_model_profile_time
86
76
 
87
77
  COMMIT;
88
78
  """
89
-
90
-
91
- def post_ddl_hook(conn: sqlite3.Connection) -> None:
92
- """S9-W1 H-DATA-01: backfill ``bytes_sha256`` for every row copied forward.
93
-
94
- The DDL INSERT could not list ``bytes_sha256`` because SQLite cannot
95
- call a Python function inside an ``executescript`` block unless the
96
- function is registered beforehand, and registering a UDF mid-DDL is
97
- fragile. Instead, we run one UPDATE pass after the DDL commits.
98
-
99
- Without this backfill, ``model_cache._parse_row`` calls
100
- ``verify_sha256(state_bytes, '')`` which raises IntegrityError, the
101
- parser tombstones the cache entry, and EVERY 18,000+ user who had a
102
- trained model on v3.4.19 loses usable learned-ranker state on upgrade.
103
-
104
- The fix is safe: SHA-256 of the already-persisted blob is
105
- deterministic, adds <1 ms per profile, and never alters
106
- ``state_bytes``. Runs inside the same connection so any UPDATE error
107
- surfaces to the runner as ``post_ddl_hook`` failed.
108
- """
109
- try:
110
- rows = conn.execute(
111
- "SELECT id, state_bytes FROM learning_model_state "
112
- "WHERE bytes_sha256 = '' OR bytes_sha256 IS NULL"
113
- ).fetchall()
114
- except sqlite3.Error:
115
- return # table empty or schema not yet present — nothing to do.
116
-
117
- if not rows:
118
- return
119
-
120
- updates = []
121
- for row_id, state_bytes in rows:
122
- if state_bytes is None:
123
- continue
124
- sha = hashlib.sha256(state_bytes).hexdigest()
125
- updates.append((sha, row_id))
126
-
127
- if updates:
128
- conn.executemany(
129
- "UPDATE learning_model_state SET bytes_sha256 = ? WHERE id = ?",
130
- updates,
131
- )
132
- conn.commit()