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
@@ -25,6 +25,44 @@ def _get_engine(request: Request):
25
25
  return get_engine_lazy(request.app.state)
26
26
 
27
27
 
28
+ def _authorize_memory_mutation(
29
+ request: Request,
30
+ operation: str,
31
+ fact_id: str,
32
+ *,
33
+ content_preview: str = "",
34
+ run_pre_hook: bool = True,
35
+ ):
36
+ """Authenticate a mutation, optionally gating route-owned direct SQL."""
37
+ from superlocalmemory.server.write_identity import require_write_actor
38
+
39
+ actor_id = require_write_actor(
40
+ request,
41
+ getattr(request.app.state, "daemon_descriptor", None),
42
+ actor_kind="dashboard",
43
+ )
44
+ engine = _get_engine(request)
45
+ if engine is None:
46
+ raise HTTPException(503, detail="Engine not initialized")
47
+ profile_id = engine.profile_id
48
+ context = {
49
+ "operation": operation,
50
+ "agent_id": actor_id,
51
+ "source_agent_id": "dashboard",
52
+ "profile_id": profile_id,
53
+ "fact_id": fact_id,
54
+ }
55
+ if content_preview:
56
+ context["content_preview"] = content_preview[:100]
57
+ if run_pre_hook:
58
+ try:
59
+ engine._hooks.run_pre(operation, context)
60
+ except Exception as exc:
61
+ logger.warning("Dashboard %s authorization rejected: %s", operation, exc)
62
+ raise HTTPException(403, detail="Write authorization rejected") from exc
63
+ return engine, profile_id, context
64
+
65
+
28
66
  def _preview(content: str | None) -> str:
29
67
  """Truncate content for preview display."""
30
68
  if not content:
@@ -430,23 +468,24 @@ async def search_memories(request: Request, body: SearchRequest):
430
468
  lambda: engine.recall(body.query, limit=body.limit, fast=False),
431
469
  )
432
470
  elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
433
- results = []
434
- for r in response.results[: body.limit]:
435
- results.append({
436
- "fact_id": r.fact.fact_id,
437
- "memory_id": getattr(r.fact, "memory_id", ""),
438
- "content": r.fact.content[:300],
439
- "score": round(r.score, 4),
440
- "confidence": round(getattr(r, "confidence", 0.0), 4),
441
- "channel_scores": getattr(r, "channel_scores", {}),
442
- "created_at": getattr(r.fact, "created_at", ""),
443
- })
471
+ from superlocalmemory.server.recall_serializer import (
472
+ recall_response_metadata,
473
+ serialize_recall_response,
474
+ )
475
+ results, no_confident_match = serialize_recall_response(
476
+ response,
477
+ limit=body.limit,
478
+ per_fact_max=300,
479
+ total_max=max(300, body.limit * 300),
480
+ )
444
481
  return {
445
482
  "query": body.query,
446
483
  "results": results,
447
484
  "total": len(results),
448
485
  "query_type": getattr(response, "query_type", "semantic"),
449
486
  "retrieval_time_ms": elapsed_ms,
487
+ "no_confident_match": no_confident_match,
488
+ **recall_response_metadata(response),
450
489
  }
451
490
 
452
491
  # Fallback: direct DB text search (engine not yet initialised)
@@ -455,18 +494,35 @@ async def search_memories(request: Request, body: SearchRequest):
455
494
  cursor = conn.cursor()
456
495
  active_profile = get_active_profile()
457
496
  cursor.execute("""
458
- SELECT fact_id as id, content, confidence as score,
497
+ SELECT fact_id, content, confidence as memory_confidence,
459
498
  fact_type as category, created_at
460
499
  FROM atomic_facts
461
500
  WHERE profile_id = ? AND content LIKE ?
462
501
  ORDER BY confidence DESC LIMIT ?
463
502
  """, (active_profile, f'%{body.query}%', body.limit))
464
- results = cursor.fetchall()
503
+ rows = cursor.fetchall()
465
504
  conn.close()
466
505
 
506
+ results = [{
507
+ **row,
508
+ "score": None,
509
+ "relevance_score": None,
510
+ "ranking_score": None,
511
+ "confidence": row.get("memory_confidence"),
512
+ "rank_position": position,
513
+ } for position, row in enumerate(rows, start=1)]
514
+
467
515
  return {
468
516
  "query": body.query, "results": results, "total": len(results),
469
517
  "query_type": "text_search", "retrieval_time_ms": 0,
518
+ "retrieval_mode": "degraded_lexical",
519
+ "score_contract_version": "2",
520
+ "calibration_status": "uncalibrated",
521
+ "calibration_id": None,
522
+ "answer_confidence": None,
523
+ "abstained": not bool(results),
524
+ "abstention_reason": None if results else "no_candidates",
525
+ "no_confident_match": False,
470
526
  }
471
527
 
472
528
  except Exception as e:
@@ -739,22 +795,20 @@ async def get_fact_detail(request: Request, fact_id: str):
739
795
  @router.delete("/api/memories/{fact_id}")
740
796
  async def delete_memory(request: Request, fact_id: str):
741
797
  """Delete a specific memory (atomic fact) by ID."""
798
+ engine, _active_profile, hook_context = _authorize_memory_mutation(
799
+ request, "delete", fact_id, run_pre_hook=False,
800
+ )
742
801
  try:
743
- conn = get_db_connection()
744
- conn.row_factory = dict_factory
745
- cursor = conn.cursor()
746
- active_profile = get_active_profile()
747
- # Verify it exists and belongs to this profile
748
- cursor.execute(
749
- "SELECT fact_id FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
750
- (fact_id, active_profile),
802
+ from superlocalmemory.core.mutations import delete_fact_authorized
803
+
804
+ result = delete_fact_authorized(
805
+ engine,
806
+ fact_id,
807
+ trusted_actor_id=hook_context["agent_id"],
808
+ source_agent_id="dashboard",
751
809
  )
752
- if not cursor.fetchone():
753
- conn.close()
810
+ if not result.get("ok"):
754
811
  raise HTTPException(status_code=404, detail="Memory not found")
755
- cursor.execute("DELETE FROM atomic_facts WHERE fact_id = ?", (fact_id,))
756
- conn.commit()
757
- conn.close()
758
812
  return {"success": True, "deleted": fact_id}
759
813
  except HTTPException:
760
814
  raise
@@ -772,11 +826,13 @@ async def forget_memory(request: Request, fact_id: str):
772
826
  future ``slm restore`` can bring it back.
773
827
  """
774
828
  import json as _json
829
+ engine, active_profile, hook_context = _authorize_memory_mutation(
830
+ request, "delete", fact_id,
831
+ )
775
832
  try:
776
833
  conn = get_db_connection()
777
834
  conn.row_factory = dict_factory
778
835
  cursor = conn.cursor()
779
- active_profile = get_active_profile()
780
836
  cursor.execute(
781
837
  "SELECT fact_id, content, importance, confidence, "
782
838
  " canonical_entities_json, embedding, created_at "
@@ -813,6 +869,7 @@ async def forget_memory(request: Request, fact_id: str):
813
869
  )
814
870
  conn.commit()
815
871
  conn.close()
872
+ engine._hooks.run_post("delete", hook_context)
816
873
  return {"success": True, "fact_id": fact_id, "archived_at": archived_at}
817
874
  except HTTPException:
818
875
  raise
@@ -830,6 +887,9 @@ async def merge_memory(request: Request, fact_id: str):
830
887
  the loser's ``merged_into`` column. The loser is archived so it
831
888
  no longer appears in default recall. The winner is untouched.
832
889
  """
890
+ engine, active_profile, hook_context = _authorize_memory_mutation(
891
+ request, "delete", fact_id,
892
+ )
833
893
  try:
834
894
  body = await request.json()
835
895
  kept = str((body or {}).get("into", "")).strip()
@@ -843,7 +903,6 @@ async def merge_memory(request: Request, fact_id: str):
843
903
  conn = get_db_connection()
844
904
  conn.row_factory = dict_factory
845
905
  cursor = conn.cursor()
846
- active_profile = get_active_profile()
847
906
  # Both must belong to the active profile.
848
907
  cursor.execute(
849
908
  "SELECT fact_id FROM atomic_facts "
@@ -874,6 +933,7 @@ async def merge_memory(request: Request, fact_id: str):
874
933
  )
875
934
  conn.commit()
876
935
  conn.close()
936
+ engine._hooks.run_post("delete", hook_context)
877
937
  return {
878
938
  "success": True,
879
939
  "merged": fact_id,
@@ -894,23 +954,24 @@ async def edit_memory(request: Request, fact_id: str):
894
954
  new_content = (body.get("content") or "").strip()
895
955
  if not new_content:
896
956
  raise HTTPException(status_code=400, detail="content is required")
897
- conn = get_db_connection()
898
- conn.row_factory = dict_factory
899
- cursor = conn.cursor()
900
- active_profile = get_active_profile()
901
- cursor.execute(
902
- "SELECT fact_id FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
903
- (fact_id, active_profile),
957
+ engine, _active_profile, hook_context = _authorize_memory_mutation(
958
+ request,
959
+ "update",
960
+ fact_id,
961
+ content_preview=new_content,
962
+ run_pre_hook=False,
904
963
  )
905
- if not cursor.fetchone():
906
- conn.close()
907
- raise HTTPException(status_code=404, detail="Memory not found")
908
- cursor.execute(
909
- "UPDATE atomic_facts SET content = ? WHERE fact_id = ?",
910
- (new_content, fact_id),
964
+ from superlocalmemory.core.mutations import update_fact_authorized
965
+
966
+ result = update_fact_authorized(
967
+ engine,
968
+ fact_id,
969
+ new_content,
970
+ trusted_actor_id=hook_context["agent_id"],
971
+ source_agent_id="dashboard",
911
972
  )
912
- conn.commit()
913
- conn.close()
973
+ if not result.get("ok"):
974
+ raise HTTPException(status_code=404, detail="Memory not found")
914
975
  return {"success": True, "fact_id": fact_id, "content": new_content}
915
976
  except HTTPException:
916
977
  raise
@@ -11,6 +11,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import re
14
15
  from typing import Optional
15
16
 
16
17
  from fastapi import APIRouter, HTTPException, Request
@@ -104,9 +105,38 @@ def _get_broker(request: Request):
104
105
  )
105
106
  if not presented or not hmac.compare_digest(presented, secret):
106
107
  raise HTTPException(401, detail="invalid or missing credential")
108
+ return broker
109
+
110
+ # Loopback is a transport property, not an identity. Every mesh read and
111
+ # write must prove the install/API/process capability because peer inboxes,
112
+ # coordination state, and project paths can be sensitive.
113
+ from superlocalmemory.server.write_identity import require_write_actor
114
+
115
+ require_write_actor(
116
+ request,
117
+ getattr(request.app.state, "daemon_descriptor", None),
118
+ actor_kind="mesh-route",
119
+ )
107
120
  return broker
108
121
 
109
122
 
123
+ _SECRET_KEY = re.compile(
124
+ r"(?:^|[_\-.])(api[_\-.]?key|secret|token|password|credential)(?:$|[_\-.])",
125
+ re.IGNORECASE,
126
+ )
127
+
128
+
129
+ def _reject_secret_state(key: str, value: str) -> None:
130
+ """Refuse secret material in the plaintext coordination store."""
131
+ from superlocalmemory.core.security_primitives import redact_secrets
132
+
133
+ if _SECRET_KEY.search(key) or redact_secrets(value) != value:
134
+ raise HTTPException(
135
+ 422,
136
+ detail="Mesh state is coordination metadata; secret values are prohibited",
137
+ )
138
+
139
+
110
140
  # -- Routes --
111
141
 
112
142
  @router.post("/register")
@@ -197,6 +227,7 @@ async def state_set(req: StateSetRequest, request: Request):
197
227
  broker = _get_broker(request)
198
228
  if not req.key:
199
229
  raise HTTPException(400, detail="key required")
230
+ _reject_secret_state(req.key, req.value)
200
231
  return broker.set_state(req.key, req.value, req.set_by)
201
232
 
202
233
 
@@ -13,7 +13,9 @@ is kept in sync as a cache for backward compatibility.
13
13
  import logging
14
14
  from datetime import datetime, timezone
15
15
 
16
- from fastapi import APIRouter, HTTPException
16
+ from fastapi import APIRouter, HTTPException, Request
17
+
18
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
17
19
 
18
20
  from .helpers import (
19
21
  get_db_connection, validate_profile_name,
@@ -84,7 +86,7 @@ async def list_profiles():
84
86
 
85
87
 
86
88
  @router.post("/api/profiles/{name}/switch")
87
- async def switch_profile(name: str):
89
+ async def switch_profile(name: str, request: Request):
88
90
  """Switch active memory profile (persists to both config stores)."""
89
91
  try:
90
92
  if not validate_profile_name(name):
@@ -100,6 +102,12 @@ async def switch_profile(name: str):
100
102
  detail=f"Profile '{name}' not found. Available: {available}",
101
103
  )
102
104
 
105
+ authorization = authorize_route_mutation(
106
+ request,
107
+ operation="update",
108
+ source_agent_id="http-profile-switch",
109
+ profile_id=name,
110
+ )
103
111
  previous = _load_profiles_json().get('active_profile', 'default')
104
112
  set_active_profile_everywhere(name)
105
113
 
@@ -118,6 +126,7 @@ async def switch_profile(name: str):
118
126
  "timestamp": datetime.now(timezone.utc).isoformat(),
119
127
  })
120
128
 
129
+ authorization.complete()
121
130
  return {
122
131
  "success": True, "active_profile": name,
123
132
  "previous_profile": previous, "memory_count": count,
@@ -131,7 +140,7 @@ async def switch_profile(name: str):
131
140
 
132
141
 
133
142
  @router.post("/api/profiles/create")
134
- async def create_profile(body: ProfileSwitch):
143
+ async def create_profile(body: ProfileSwitch, request: Request):
135
144
  """Create a new memory profile (writes to BOTH SQLite and profiles.json)."""
136
145
  try:
137
146
  name = body.profile_name
@@ -144,11 +153,17 @@ async def create_profile(body: ProfileSwitch):
144
153
  if name in merged_ids:
145
154
  raise HTTPException(status_code=409, detail=f"Profile '{name}' already exists")
146
155
 
156
+ authorization = authorize_route_mutation(
157
+ request,
158
+ operation="update",
159
+ source_agent_id="http-profile-create",
160
+ )
147
161
  # Write to BOTH stores atomically
148
162
  desc = f'Memory profile: {name}'
149
163
  ensure_profile_in_db(name, desc)
150
164
  ensure_profile_in_json(name, desc)
151
165
 
166
+ authorization.complete()
152
167
  return {"success": True, "profile": name, "message": f"Profile '{name}' created"}
153
168
 
154
169
  except HTTPException:
@@ -158,7 +173,7 @@ async def create_profile(body: ProfileSwitch):
158
173
 
159
174
 
160
175
  @router.delete("/api/profiles/{name}")
161
- async def delete_profile(name: str):
176
+ async def delete_profile(name: str, request: Request):
162
177
  """Delete a profile. Moves its memories to 'default'."""
163
178
  try:
164
179
  if name == 'default':
@@ -173,6 +188,12 @@ async def delete_profile(name: str):
173
188
  if json_config.get('active_profile') == name:
174
189
  raise HTTPException(status_code=400, detail="Cannot delete active profile.")
175
190
 
191
+ authorization = authorize_route_mutation(
192
+ request,
193
+ operation="delete",
194
+ source_agent_id="http-profile-delete",
195
+ profile_id=name,
196
+ )
176
197
  # Move data to default before deleting (bypasses CASCADE)
177
198
  conn = get_db_connection()
178
199
  cursor = conn.cursor()
@@ -204,6 +225,7 @@ async def delete_profile(name: str):
204
225
  json_config['profiles'] = profiles
205
226
  _save_profiles_json(json_config)
206
227
 
228
+ authorization.complete()
207
229
  return {
208
230
  "success": True,
209
231
  "message": f"Profile '{name}' deleted. {moved} memories moved to 'default'.",
@@ -18,6 +18,8 @@ from datetime import datetime, UTC
18
18
  from fastapi import APIRouter, HTTPException, Request
19
19
  from pydantic import BaseModel, Field
20
20
 
21
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
22
+
21
23
  from .helpers import DB_PATH
22
24
 
23
25
  logger = logging.getLogger("superlocalmemory.routes.tiers")
@@ -105,6 +107,12 @@ async def evaluate_tiers_route(request: Request, profile_id: str = "default"):
105
107
  profile_id = _validate_profile(profile_id)
106
108
 
107
109
  try:
110
+ authorization = authorize_route_mutation(
111
+ request,
112
+ operation="update",
113
+ source_agent_id="http-tier-evaluate",
114
+ profile_id=profile_id,
115
+ )
108
116
  from superlocalmemory.core.tier_manager import evaluate_tiers
109
117
  from .helpers import get_engine_lazy
110
118
 
@@ -114,6 +122,7 @@ async def evaluate_tiers_route(request: Request, profile_id: str = "default"):
114
122
  status_code=503, detail="Engine not initialized",
115
123
  )
116
124
  stats = evaluate_tiers(engine._db, profile_id)
125
+ authorization.complete()
117
126
  return {"success": True, "stats": stats}
118
127
  except HTTPException:
119
128
  raise
@@ -125,12 +134,23 @@ async def evaluate_tiers_route(request: Request, profile_id: str = "default"):
125
134
 
126
135
 
127
136
  @router.post("/api/tiers/pin")
128
- async def pin_fact_route(request: PinRequest, profile_id: str = "default"):
137
+ async def pin_fact_route(
138
+ body: PinRequest,
139
+ request: Request,
140
+ profile_id: str = "default",
141
+ ):
129
142
  """Pin a fact to stay in active tier forever.
130
143
 
131
144
  Validates fact exists in the specified profile before pinning.
132
145
  """
133
146
  profile_id = _validate_profile(profile_id)
147
+ authorization = authorize_route_mutation(
148
+ request,
149
+ operation="update",
150
+ source_agent_id="http-tier-pin",
151
+ profile_id=profile_id,
152
+ fact_id=body.fact_id,
153
+ )
134
154
 
135
155
  with _db() as conn:
136
156
  try:
@@ -139,12 +159,12 @@ async def pin_fact_route(request: PinRequest, profile_id: str = "default"):
139
159
  c.execute(
140
160
  "SELECT fact_id FROM atomic_facts "
141
161
  "WHERE fact_id = ? AND profile_id = ?",
142
- (request.fact_id, profile_id),
162
+ (body.fact_id, profile_id),
143
163
  )
144
164
  if c.fetchone() is None:
145
165
  raise HTTPException(
146
166
  status_code=404,
147
- detail=f"Fact {request.fact_id[:8]}... not found",
167
+ detail=f"Fact {body.fact_id[:8]}... not found",
148
168
  )
149
169
 
150
170
  now = datetime.now(UTC).isoformat()
@@ -152,15 +172,15 @@ async def pin_fact_route(request: PinRequest, profile_id: str = "default"):
152
172
  "INSERT OR REPLACE INTO pinned_facts "
153
173
  "(fact_id, profile_id, pinned_at, reason) "
154
174
  "VALUES (?, ?, ?, ?)",
155
- (request.fact_id, profile_id, now, request.reason),
175
+ (body.fact_id, profile_id, now, body.reason),
156
176
  )
157
- conn.execute(
158
- "UPDATE atomic_facts SET lifecycle = 'active' "
159
- "WHERE fact_id = ? AND profile_id = ?",
160
- (request.fact_id, profile_id),
177
+ from superlocalmemory.core.lifecycle_state import set_fact_lifecycle_zone
178
+ set_fact_lifecycle_zone(
179
+ conn, [body.fact_id], "active", profile_id=profile_id,
161
180
  )
162
181
  conn.commit()
163
- return {"success": True, "message": f"Fact {request.fact_id[:8]}... pinned"}
182
+ authorization.complete()
183
+ return {"success": True, "message": f"Fact {body.fact_id[:8]}... pinned"}
164
184
  except HTTPException:
165
185
  raise
166
186
  except Exception as exc:
@@ -171,7 +191,11 @@ async def pin_fact_route(request: PinRequest, profile_id: str = "default"):
171
191
 
172
192
 
173
193
  @router.post("/api/tiers/unpin")
174
- async def unpin_fact_route(request: PinRequest, profile_id: str = "default"):
194
+ async def unpin_fact_route(
195
+ body: PinRequest,
196
+ request: Request,
197
+ profile_id: str = "default",
198
+ ):
175
199
  """Unpin a fact, allowing normal tier demotion.
176
200
 
177
201
  Lifecycle stays 'active' until the next tier evaluation cycle demotes it
@@ -179,14 +203,22 @@ async def unpin_fact_route(request: PinRequest, profile_id: str = "default"):
179
203
  surprise the user.
180
204
  """
181
205
  profile_id = _validate_profile(profile_id)
206
+ authorization = authorize_route_mutation(
207
+ request,
208
+ operation="update",
209
+ source_agent_id="http-tier-unpin",
210
+ profile_id=profile_id,
211
+ fact_id=body.fact_id,
212
+ )
182
213
 
183
214
  with _db() as conn:
184
215
  try:
185
216
  conn.execute(
186
217
  "DELETE FROM pinned_facts WHERE fact_id = ? AND profile_id = ?",
187
- (request.fact_id, profile_id),
218
+ (body.fact_id, profile_id),
188
219
  )
189
220
  conn.commit()
221
+ authorization.complete()
190
222
  return {"success": True, "unpinned": True}
191
223
  except Exception as exc:
192
224
  logger.error("unpin_fact failed: %s", exc, exc_info=True)
@@ -24,7 +24,11 @@ logger = logging.getLogger(__name__)
24
24
  router = APIRouter(prefix="/api/v3/timeline", tags=["timeline"])
25
25
 
26
26
  VALID_RANGES = {"1d", "7d", "30d", "90d", "365d"}
27
- VALID_GROUP_BY = {"category", "community"}
27
+ # ``date`` is the dashboard's chronological view. It does not need an extra
28
+ # enrichment pass because each returned event already carries its timestamp,
29
+ # but it is a first-class grouping selection and must not be rejected at the
30
+ # API boundary.
31
+ VALID_GROUP_BY = {"category", "community", "date"}
28
32
  INTERNAL_CEILING = 5000
29
33
 
30
34