superlocalmemory 3.7.8 → 3.8.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 (260) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +69 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -1
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +94 -10
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/backup.py +12 -1
  116. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  117. package/src/superlocalmemory/infra/data_root.py +43 -4
  118. package/src/superlocalmemory/infra/event_bus.py +107 -24
  119. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  120. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  121. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  122. package/src/superlocalmemory/learning/cross_project.py +28 -19
  123. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  124. package/src/superlocalmemory/loops/__init__.py +56 -0
  125. package/src/superlocalmemory/loops/budget.py +58 -0
  126. package/src/superlocalmemory/loops/engine.py +164 -0
  127. package/src/superlocalmemory/loops/ledger.py +243 -0
  128. package/src/superlocalmemory/loops/models.py +152 -0
  129. package/src/superlocalmemory/loops/rules.py +52 -0
  130. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  131. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  132. package/src/superlocalmemory/mcp/profiles.py +103 -0
  133. package/src/superlocalmemory/mcp/server.py +21 -49
  134. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  135. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  136. package/src/superlocalmemory/mcp/tools_core.py +8 -1
  137. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  138. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  139. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  140. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  141. package/src/superlocalmemory/mesh/broker.py +237 -129
  142. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  143. package/src/superlocalmemory/optimize/NOTICE +1 -6
  144. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  145. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  147. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  148. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  149. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  150. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  151. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  152. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  153. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  154. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  155. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  156. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  157. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  158. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  159. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  160. package/src/superlocalmemory/retrieval/engine.py +168 -26
  161. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  162. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  163. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  164. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  165. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  166. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  167. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  168. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  169. package/src/superlocalmemory/server/api.py +4 -4
  170. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  171. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  172. package/src/superlocalmemory/server/recall_health.py +24 -3
  173. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  174. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  175. package/src/superlocalmemory/server/routes/agents.py +128 -38
  176. package/src/superlocalmemory/server/routes/backup.py +34 -10
  177. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  178. package/src/superlocalmemory/server/routes/brain.py +21 -5
  179. package/src/superlocalmemory/server/routes/chat.py +10 -5
  180. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  181. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  183. package/src/superlocalmemory/server/routes/entity.py +9 -4
  184. package/src/superlocalmemory/server/routes/events.py +24 -8
  185. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  186. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  187. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  188. package/src/superlocalmemory/server/routes/insights.py +3 -3
  189. package/src/superlocalmemory/server/routes/learning.py +14 -14
  190. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  191. package/src/superlocalmemory/server/routes/memories.py +182 -57
  192. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  193. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  194. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  195. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  196. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  197. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  198. package/src/superlocalmemory/server/routes/stats.py +13 -6
  199. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  200. package/src/superlocalmemory/server/routes/v3_api.py +183 -69
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +384 -56
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  208. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  209. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  210. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  211. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  212. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  213. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  214. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  215. package/src/superlocalmemory/storage/models.py +4 -0
  216. package/src/superlocalmemory/storage/schema.py +87 -0
  217. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  218. package/src/superlocalmemory/trust/gate.py +49 -8
  219. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  220. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  221. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  222. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  223. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  224. package/src/superlocalmemory/ui/favicon.svg +35 -4
  225. package/src/superlocalmemory/ui/index.html +306 -173
  226. package/src/superlocalmemory/ui/js/brain.js +5 -20
  227. package/src/superlocalmemory/ui/js/core.js +47 -31
  228. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  229. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  230. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  231. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  232. package/src/superlocalmemory/ui/js/memories.js +15 -4
  233. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  234. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  235. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  236. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  237. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  238. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  239. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  240. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  241. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  242. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  243. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  244. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  245. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  246. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  247. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  248. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  249. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  250. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  251. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  252. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  253. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  254. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  255. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  256. package/src/superlocalmemory/ui/js/settings.js +2 -2
  257. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  258. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  259. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  260. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -19,6 +19,12 @@ from .helpers import DB_PATH
19
19
  logger = logging.getLogger("superlocalmemory.routes.agents")
20
20
  router = APIRouter()
21
21
 
22
+
23
+ def _internal_error(detail: str = "Internal server error") -> HTTPException:
24
+ """SEC-H-02: log full traceback server-side; return a generic message to the client."""
25
+ logger.exception("agents route error")
26
+ return HTTPException(status_code=500, detail=detail)
27
+
22
28
  # Feature flag: V3 trust scorer
23
29
  TRUST_AVAILABLE = False
24
30
  try:
@@ -56,8 +62,8 @@ async def get_agents(
56
62
  "count": len(agents),
57
63
  "stats": {"total_agents": len(agents)},
58
64
  }
59
- except Exception as e:
60
- raise HTTPException(status_code=500, detail=f"Agent registry error: {str(e)}")
65
+ except Exception:
66
+ raise _internal_error("Agent registry error")
61
67
 
62
68
 
63
69
  @router.get("/api/agents/stats")
@@ -69,8 +75,91 @@ async def get_agent_stats(request: Request):
69
75
  registry = AgentRegistry(persist_path=_registry_path())
70
76
  agents = registry.list_agents()
71
77
  return {"total_agents": len(agents)}
72
- except Exception as e:
73
- raise HTTPException(status_code=500, detail=f"Agent stats error: {str(e)}")
78
+ except Exception:
79
+ raise _internal_error("Agent stats error")
80
+
81
+
82
+ @router.get("/api/agents/memory-activity")
83
+ async def get_agent_memory_activity(
84
+ request: Request,
85
+ limit: int = Query(20, ge=1, le=100),
86
+ ):
87
+ """Per-agent memory attribution for the multi-agent memory view.
88
+
89
+ Reports how many memories each writing agent contributed, when each was
90
+ last active, which ingestion sources they used, and the most recent
91
+ entries — grouped by ``ingestion_operations.trusted_actor_id`` (the agent
92
+ that wrote the memory). Profile-scoped. Uses a direct DB read because the
93
+ dashboard runs without the engine subprocess. Never raises to the client;
94
+ returns empty structures if the operations table is absent.
95
+ """
96
+ import sqlite3
97
+
98
+ from .helpers import get_active_profile
99
+
100
+ pid = get_active_profile()
101
+ agents: list[dict] = []
102
+ recent: list[dict] = []
103
+ total = 0
104
+
105
+ if DB_PATH.exists():
106
+ conn = sqlite3.connect(str(DB_PATH))
107
+ conn.row_factory = sqlite3.Row
108
+ try:
109
+ try:
110
+ rows = conn.execute(
111
+ "SELECT CASE WHEN trusted_actor_id='' THEN 'unknown' "
112
+ "ELSE trusted_actor_id END AS agent_id, "
113
+ "COUNT(*) AS cnt, MAX(created_at) AS last_active, "
114
+ "GROUP_CONCAT(DISTINCT source_type) AS sources "
115
+ "FROM ingestion_operations WHERE profile_id=? "
116
+ "GROUP BY agent_id ORDER BY cnt DESC, agent_id ASC "
117
+ "LIMIT 500",
118
+ (pid,),
119
+ ).fetchall()
120
+ for r in rows:
121
+ agents.append({
122
+ "agent_id": r["agent_id"],
123
+ "count": r["cnt"],
124
+ "last_active": r["last_active"],
125
+ "source_types": (
126
+ [s for s in (r["sources"] or "").split(",") if s]
127
+ ),
128
+ })
129
+ total += r["cnt"]
130
+ except sqlite3.OperationalError:
131
+ pass
132
+
133
+ try:
134
+ rows = conn.execute(
135
+ "SELECT CASE WHEN trusted_actor_id='' THEN 'unknown' "
136
+ "ELSE trusted_actor_id END AS agent_id, "
137
+ "substr(raw_content, 1, 160) AS snippet, "
138
+ "created_at, source_type, session_id "
139
+ "FROM ingestion_operations WHERE profile_id=? "
140
+ "ORDER BY created_at DESC, rowid DESC LIMIT ?",
141
+ (pid, int(limit)),
142
+ ).fetchall()
143
+ recent = [{
144
+ "agent_id": r["agent_id"],
145
+ "content": r["snippet"],
146
+ "created_at": r["created_at"],
147
+ "source_type": r["source_type"],
148
+ "session_id": r["session_id"],
149
+ } for r in rows]
150
+ except sqlite3.OperationalError:
151
+ pass
152
+ finally:
153
+ conn.close()
154
+
155
+ return {
156
+ "ok": True,
157
+ "profile_id": pid,
158
+ "total_memories": total,
159
+ "agent_count": len(agents),
160
+ "agents": agents,
161
+ "recent": recent,
162
+ }
74
163
 
75
164
 
76
165
  @router.get("/api/trust/stats")
@@ -102,38 +191,39 @@ async def get_trust_stats(request: Request):
102
191
  conn = sqlite3.connect(str(DB_PATH))
103
192
  conn.row_factory = sqlite3.Row
104
193
  try:
105
- # Count trust signals
106
- row = conn.execute(
107
- "SELECT COUNT(*) AS cnt FROM trust_signals "
108
- "WHERE profile_id = ?", (pid,),
109
- ).fetchone()
110
- total_signals = row["cnt"] if row else 0
111
- except sqlite3.OperationalError:
112
- pass
113
-
114
- try:
115
- # Average trust score
116
- row = conn.execute(
117
- "SELECT AVG(trust_score) AS avg_ts FROM trust_scores "
118
- "WHERE profile_id = ?", (pid,),
119
- ).fetchone()
120
- if row and row["avg_ts"] is not None:
121
- avg_trust_score = round(float(row["avg_ts"]), 3)
122
- except sqlite3.OperationalError:
123
- pass
194
+ try:
195
+ # Count trust signals
196
+ row = conn.execute(
197
+ "SELECT COUNT(*) AS cnt FROM trust_signals "
198
+ "WHERE profile_id = ?", (pid,),
199
+ ).fetchone()
200
+ total_signals = row["cnt"] if row else 0
201
+ except sqlite3.OperationalError:
202
+ pass
124
203
 
125
- try:
126
- # Signal breakdown by type
127
- rows = conn.execute(
128
- "SELECT signal_type, COUNT(*) AS cnt "
129
- "FROM trust_signals WHERE profile_id = ? "
130
- "GROUP BY signal_type", (pid,),
131
- ).fetchall()
132
- by_signal_type = {r["signal_type"]: r["cnt"] for r in rows}
133
- except sqlite3.OperationalError:
134
- pass
204
+ try:
205
+ # Average trust score
206
+ row = conn.execute(
207
+ "SELECT AVG(trust_score) AS avg_ts FROM trust_scores "
208
+ "WHERE profile_id = ?", (pid,),
209
+ ).fetchone()
210
+ if row and row["avg_ts"] is not None:
211
+ avg_trust_score = round(float(row["avg_ts"]), 3)
212
+ except sqlite3.OperationalError:
213
+ pass
135
214
 
136
- conn.close()
215
+ try:
216
+ # Signal breakdown by type
217
+ rows = conn.execute(
218
+ "SELECT signal_type, COUNT(*) AS cnt "
219
+ "FROM trust_signals WHERE profile_id = ? "
220
+ "GROUP BY signal_type", (pid,),
221
+ ).fetchall()
222
+ by_signal_type = {r["signal_type"]: r["cnt"] for r in rows}
223
+ except sqlite3.OperationalError:
224
+ pass
225
+ finally:
226
+ conn.close()
137
227
 
138
228
  # Enforcement status: SLM uses "Silent Collection" by default
139
229
  enforcement = "Silent Collection"
@@ -144,8 +234,8 @@ async def get_trust_stats(request: Request):
144
234
  "enforcement": enforcement,
145
235
  "by_signal_type": by_signal_type,
146
236
  }
147
- except Exception as e:
148
- raise HTTPException(status_code=500, detail=f"Trust stats error: {str(e)}")
237
+ except Exception:
238
+ raise _internal_error("Trust stats error")
149
239
 
150
240
 
151
241
  @router.get("/api/trust/signals/{agent_id}")
@@ -167,5 +257,5 @@ async def get_agent_trust_signals(
167
257
  "signals": signals, "count": len(signals),
168
258
  }
169
259
  return {"agent_id": agent_id, "signals": [], "count": 0}
170
- except Exception as e:
171
- raise HTTPException(status_code=500, detail=f"Trust signals error: {str(e)}")
260
+ except Exception:
261
+ raise _internal_error("Trust signals error")
@@ -24,6 +24,13 @@ from .helpers import BackupConfigRequest, DB_PATH, MEMORY_DIR
24
24
  logger = logging.getLogger("superlocalmemory.routes.backup")
25
25
  router = APIRouter()
26
26
 
27
+
28
+ def _internal_error(detail: str = "Internal server error") -> HTTPException:
29
+ """SEC-H-02: log full traceback server-side; return a generic message to the client."""
30
+ logger.exception("backup route error")
31
+ return HTTPException(status_code=500, detail=detail)
32
+
33
+
27
34
  # Feature flags
28
35
  BACKUP_AVAILABLE = False
29
36
  CLOUD_AVAILABLE = False
@@ -79,8 +86,8 @@ async def backup_status():
79
86
  else:
80
87
  status["cloud_destinations"] = []
81
88
  return status
82
- except Exception as e:
83
- raise HTTPException(status_code=500, detail=f"Backup status error: {str(e)}")
89
+ except Exception:
90
+ raise _internal_error("Backup status error")
84
91
 
85
92
 
86
93
  @router.post("/api/backup/create")
@@ -98,8 +105,8 @@ async def backup_create():
98
105
  "status": manager.get_status(),
99
106
  }
100
107
  return {"success": False, "message": "Backup failed"}
101
- except Exception as e:
102
- raise HTTPException(status_code=500, detail=f"Backup create error: {str(e)}")
108
+ except Exception:
109
+ raise _internal_error("Backup create error")
103
110
 
104
111
 
105
112
  @router.post("/api/backup/configure")
@@ -115,21 +122,30 @@ async def backup_configure(request: BackupConfigRequest):
115
122
  enabled=request.enabled,
116
123
  )
117
124
  return {"success": True, "message": "Backup configuration updated", "status": result}
118
- except Exception as e:
119
- raise HTTPException(status_code=500, detail=f"Backup configure error: {str(e)}")
125
+ except Exception:
126
+ raise _internal_error("Backup configure error")
120
127
 
121
128
 
122
129
  @router.get("/api/backup/list")
123
- async def backup_list():
130
+ async def backup_list(request: Request):
124
131
  """List all available backups."""
125
132
  if not BACKUP_AVAILABLE:
126
133
  return {"backups": [], "count": 0, "message": "Backup module not available"}
134
+ # This GET is not covered by the mutation middleware. The local machine
135
+ # owner (loopback) is trusted; a remote caller must present a credential;
136
+ # non-loopback uncredentialed callers fail closed. Using the mutation-actor
137
+ # boundary (not require_write_actor) so the same-origin dashboard — whose
138
+ # fetch wrapper only attaches the install token to mutating requests — can
139
+ # still read its own backup list.
140
+ from superlocalmemory.server.write_identity import require_http_mutation_actor
141
+ require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
142
+ actor_kind="backup-list")
127
143
  try:
128
144
  manager = _get_backup_manager()
129
145
  backups = manager.list_backups()
130
146
  return {"backups": backups, "count": len(backups)}
131
- except Exception as e:
132
- raise HTTPException(status_code=500, detail=f"Backup list error: {str(e)}")
147
+ except Exception:
148
+ raise _internal_error("Backup list error")
133
149
 
134
150
 
135
151
  # ---- Cloud destination routes (v3.4.10) -----------------------------------
@@ -228,10 +244,18 @@ async def sync_cloud():
228
244
  # ---- Export / Download route (v3.4.10) ------------------------------------
229
245
 
230
246
  @router.get("/api/backup/export")
231
- async def export_backup():
247
+ async def export_backup(request: Request):
232
248
  """Create and download a compressed backup archive."""
233
249
  if not BACKUP_AVAILABLE:
234
250
  raise HTTPException(status_code=501, detail="Backup module not available")
251
+ # Full-database download. This GET is not covered by the mutation
252
+ # middleware and is triggered by a top-level navigation (window.location),
253
+ # which cannot carry a custom credential header — so it uses the
254
+ # loopback-trusted mutation-actor boundary: the local owner may export,
255
+ # a non-loopback caller without a credential fails closed.
256
+ from superlocalmemory.server.write_identity import require_http_mutation_actor
257
+ require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
258
+ actor_kind="backup-export")
235
259
 
236
260
  manager = _get_backup_manager()
237
261
  filename = manager.create_backup(label="export")
@@ -90,8 +90,8 @@ async def behavioral_status():
90
90
  },
91
91
  }
92
92
  except Exception as e:
93
- logger.error("behavioral_status error: %s", e)
94
- return {"available": False, "error": str(e)}
93
+ logger.exception("behavioral_status error")
94
+ return {"available": False, "error": "Internal server error"}
95
95
 
96
96
 
97
97
  @router.post("/api/behavioral/report-outcome")
@@ -173,8 +173,8 @@ async def report_outcome(data: dict):
173
173
  ),
174
174
  }
175
175
  except Exception as e:
176
- logger.error("report_outcome error: %s", e)
177
- return {"success": False, "error": str(e)}
176
+ logger.exception("report_outcome error")
177
+ return {"success": False, "error": "Internal server error"}
178
178
 
179
179
 
180
180
  # --------------------------------------------------------------------------
@@ -214,8 +214,8 @@ async def get_assertions(min_confidence: float = 0.0, category: str = "", limit:
214
214
  "active_profile": profile,
215
215
  }
216
216
  except Exception as e:
217
- logger.debug("get_assertions error: %s", e)
218
- return {"assertions": [], "count": 0, "error": str(e)}
217
+ logger.exception("get_assertions error")
218
+ return {"assertions": [], "count": 0, "error": "Internal server error"}
219
219
 
220
220
 
221
221
  @router.get("/api/behavioral/tool-events")
@@ -247,8 +247,8 @@ async def get_tool_events(tool_name: str = "", limit: int = 100):
247
247
  finally:
248
248
  conn.close()
249
249
  except Exception as e:
250
- logger.debug("get_tool_events error: %s", e)
251
- return {"events": [], "count": 0, "error": str(e)}
250
+ logger.exception("get_tool_events error")
251
+ return {"events": [], "count": 0, "error": "Internal server error"}
252
252
 
253
253
 
254
254
  @router.get("/api/behavioral/soft-prompts")
@@ -269,8 +269,8 @@ async def get_soft_prompts():
269
269
  "token_count", "active", "version", "created_at"], r
270
270
  )) for r in rows], "count": len(rows)}
271
271
  except Exception as e:
272
- logger.debug("get_soft_prompts error: %s", e)
273
- return {"prompts": [], "count": 0, "error": str(e)}
272
+ logger.exception("get_soft_prompts error")
273
+ return {"prompts": [], "count": 0, "error": "Internal server error"}
274
274
 
275
275
 
276
276
  @router.post("/api/v3/tool-event")
@@ -322,5 +322,6 @@ async def log_tool_event_api(data: dict):
322
322
  finally:
323
323
  conn.close()
324
324
  return {"ok": True}
325
- except Exception as e:
326
- return {"ok": False, "error": str(e)}
325
+ except Exception:
326
+ logger.exception("behavioral route error")
327
+ return {"ok": False, "error": "Internal server error"}
@@ -30,6 +30,14 @@ Design notes (LLD-04 §7 hard rules):
30
30
  never spell those names anywhere in this module.
31
31
  * **U6** — install token required on ``/api/v3/brain`` and all
32
32
  deprecated shim routes. See ``require_install_token`` below.
33
+
34
+ External API consumers (L1): the dashboard auto-fetches the token from
35
+ ``GET /internal/token`` (loopback + same-origin only) and sends it as the
36
+ ``X-Install-Token`` header. Non-browser callers (scripts, IDE adapters,
37
+ third-party integrations) instead read the token from
38
+ ``~/.superlocalmemory/.install_token`` and send the same header:
39
+ ``curl -H "X-Install-Token: $(cat ~/.superlocalmemory/.install_token)" \
40
+ http://127.0.0.1:8765/api/v3/brain``.
33
41
  * **U10** — feature count surfaced from ``features.FEATURE_DIM``;
34
42
  stratum total from module constant ``_STRATA_TOTAL`` (48 = 4×3×4).
35
43
  """
@@ -52,6 +60,7 @@ from superlocalmemory.core.security_primitives import (
52
60
  from superlocalmemory.learning.database import LearningDatabase
53
61
  from superlocalmemory.learning.features import FEATURE_DIM
54
62
  from superlocalmemory.infra.data_root import canonical_data_root
63
+ from .helpers import get_active_profile
55
64
 
56
65
  logger = logging.getLogger("superlocalmemory.routes.brain")
57
66
 
@@ -900,7 +909,7 @@ def _action_outcomes_count(lrn_db: LearningDatabase,
900
909
 
901
910
 
902
911
  @router.get("/brain", dependencies=[Depends(require_install_token)])
903
- async def get_brain(profile_id: str = "default") -> dict:
912
+ async def get_brain(profile_id: str | None = None) -> dict:
904
913
  """Unified Brain endpoint — LLD-04 §3.1.
905
914
 
906
915
  Fan-out: each section is a synchronous SQLite reader. Running them
@@ -915,6 +924,9 @@ async def get_brain(profile_id: str = "default") -> dict:
915
924
  """
916
925
  import asyncio
917
926
 
927
+ # Default to the ACTIVE profile (request runtime truth), never literal
928
+ # "default" — the Brain must reflect whichever profile is active.
929
+ profile_id = profile_id or get_active_profile()
918
930
  lrn_db = LearningDatabase(_learning_db_path())
919
931
 
920
932
  (
@@ -1176,7 +1188,7 @@ def _compute_evolution_cost_preview(profile_id: str) -> dict:
1176
1188
  @router.get("/brain/evolution-timeseries",
1177
1189
  dependencies=[Depends(require_install_token)])
1178
1190
  async def get_brain_evolution_timeseries(
1179
- profile_id: str = "default",
1191
+ profile_id: str | None = None,
1180
1192
  days: int = _EVOLUTION_DEFAULT_DAYS,
1181
1193
  ) -> dict:
1182
1194
  """Daily learning-signal counts for ``profile_id`` over the last ``days``.
@@ -1186,6 +1198,7 @@ async def get_brain_evolution_timeseries(
1186
1198
  """
1187
1199
  import asyncio
1188
1200
 
1201
+ profile_id = profile_id or get_active_profile()
1189
1202
  lrn_db = LearningDatabase(_learning_db_path())
1190
1203
  result = await asyncio.to_thread(
1191
1204
  _compute_evolution_timeseries, profile_id, lrn_db, days=days,
@@ -1201,7 +1214,8 @@ async def get_brain_evolution_timeseries(
1201
1214
 
1202
1215
  @router.get("/learning/stats",
1203
1216
  dependencies=[Depends(require_install_token)])
1204
- async def learning_stats_deprecated(profile_id: str = "default") -> dict:
1217
+ async def learning_stats_deprecated(profile_id: str | None = None) -> dict:
1218
+ profile_id = profile_id or get_active_profile()
1205
1219
  lrn_db = LearningDatabase(_learning_db_path())
1206
1220
  return {
1207
1221
  "deprecated": True,
@@ -1212,7 +1226,8 @@ async def learning_stats_deprecated(profile_id: str = "default") -> dict:
1212
1226
 
1213
1227
  @router.get("/patterns",
1214
1228
  dependencies=[Depends(require_install_token)])
1215
- async def patterns_deprecated(profile_id: str = "default") -> dict:
1229
+ async def patterns_deprecated(profile_id: str | None = None) -> dict:
1230
+ profile_id = profile_id or get_active_profile()
1216
1231
  return {
1217
1232
  "deprecated": True,
1218
1233
  "use_instead": "/api/v3/brain",
@@ -1222,7 +1237,8 @@ async def patterns_deprecated(profile_id: str = "default") -> dict:
1222
1237
 
1223
1238
  @router.get("/behavioral",
1224
1239
  dependencies=[Depends(require_install_token)])
1225
- async def behavioral_deprecated(profile_id: str = "default") -> dict:
1240
+ async def behavioral_deprecated(profile_id: str | None = None) -> dict:
1241
+ profile_id = profile_id or get_active_profile()
1226
1242
  return {
1227
1243
  "deprecated": True,
1228
1244
  "use_instead": "/api/v3/brain",
@@ -101,8 +101,9 @@ async def _stream_chat(
101
101
  memories = await loop.run_in_executor(
102
102
  None, _recall_memories, app_state, query, limit,
103
103
  )
104
- except Exception as exc:
105
- yield _sse_event("error", json.dumps({"message": f"Retrieval failed: {exc}"}))
104
+ except Exception:
105
+ logger.exception("chat: memory retrieval failed")
106
+ yield _sse_event("error", json.dumps({"message": "Memory retrieval failed"}))
106
107
  yield _sse_event("done", "")
107
108
  return
108
109
 
@@ -226,8 +227,9 @@ async def _stream_mode_bc(
226
227
  yield _sse_event("token", token)
227
228
  except httpx.ConnectError:
228
229
  yield _sse_event("token", f"\n\n[Connection failed — is {provider} running?]")
229
- except Exception as exc:
230
- yield _sse_event("token", f"\n\n[LLM error: {exc}]")
230
+ except Exception:
231
+ logger.exception("chat: LLM streaming error")
232
+ yield _sse_event("token", "\n\n[LLM error: request failed]")
231
233
 
232
234
 
233
235
  # ── Ollama Streaming (/api/chat with messages) ───────────────────
@@ -359,7 +361,10 @@ def _recall_via_resident_engine(app_state, query: str, limit: int) -> list:
359
361
  if r.fact.memory_id
360
362
  })
361
363
  memory_map = (
362
- engine._db.get_memory_content_batch(memory_ids) if memory_ids else {}
364
+ engine._db.get_memory_content_batch(
365
+ memory_ids, engine.profile_id,
366
+ include_global=True, include_shared=True,
367
+ ) if memory_ids else {}
363
368
  )
364
369
  from superlocalmemory.server.recall_serializer import (
365
370
  serialize_recall_response,