superlocalmemory 3.7.8 → 3.8.1

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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  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/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  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 +827 -72
  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 +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -4,23 +4,86 @@
4
4
  """SuperLocalMemory V3 - Stats Routes
5
5
  - AGPL-3.0-or-later
6
6
 
7
- Routes: /api/stats, /api/timeline, /api/patterns
7
+ Routes: /api/stats, /api/timeline
8
8
  """
9
- import json
10
9
  import logging
11
- from collections import defaultdict
10
+ import sqlite3
12
11
  from typing import Optional
13
12
 
14
13
  from fastapi import APIRouter, HTTPException, Query
15
14
 
16
- from .helpers import get_db_connection, dict_factory, get_active_profile, DB_PATH, MEMORY_DIR
15
+ from .helpers import get_db_connection, dict_factory, get_active_profile, DB_PATH
17
16
 
18
17
  logger = logging.getLogger("superlocalmemory.routes.stats")
19
18
  router = APIRouter()
20
19
 
21
20
 
21
+ def _query_ingestion_sources(cursor, profile_id: str) -> list[dict]:
22
+ """Query profile-scoped ingestion provenance without double-counting facts."""
23
+ cursor.execute(
24
+ """
25
+ SELECT COALESCE(NULLIF(TRIM(source_type), ''), 'unknown') AS source_type,
26
+ COUNT(DISTINCT fact_id) AS count
27
+ FROM provenance
28
+ WHERE profile_id = ?
29
+ GROUP BY COALESCE(NULLIF(TRIM(source_type), ''), 'unknown')
30
+ ORDER BY count DESC, source_type ASC
31
+ """,
32
+ (profile_id,),
33
+ )
34
+ return [dict(row) for row in cursor.fetchall()]
35
+
36
+
37
+ def _load_ingestion_sources_with_status(
38
+ cursor, profile_id: str,
39
+ ) -> tuple[list[dict], dict]:
40
+ """Return provenance plus a truthful availability state.
41
+
42
+ Older databases may not have the provenance table. In that case the
43
+ dashboard must report an empty/unknown state rather than inventing a split.
44
+ Lock or I/O failures are distinct from a legitimate legacy-empty state.
45
+ """
46
+ try:
47
+ return _query_ingestion_sources(cursor, profile_id), {
48
+ "available": True,
49
+ "state": "recorded",
50
+ "source": "memory.db:provenance",
51
+ }
52
+ except sqlite3.OperationalError as exc:
53
+ if "no such table" in str(exc).lower():
54
+ return [], {
55
+ "available": True,
56
+ "state": "legacy_not_recorded",
57
+ "source": "memory.db:provenance",
58
+ }
59
+ logger.warning("provenance metrics temporarily unavailable: %s", exc)
60
+ return [], {
61
+ "available": False,
62
+ "state": "temporarily_unavailable",
63
+ "source": "memory.db:provenance",
64
+ }
65
+ except sqlite3.Error as exc:
66
+ logger.warning("provenance metrics unavailable: %s", exc)
67
+ return [], {
68
+ "available": False,
69
+ "state": "temporarily_unavailable",
70
+ "source": "memory.db:provenance",
71
+ }
72
+
73
+
74
+ def _load_ingestion_sources(cursor, profile_id: str) -> list[dict]:
75
+ """Compatibility helper returning only the provenance rows."""
76
+ return _load_ingestion_sources_with_status(cursor, profile_id)[0]
77
+
78
+
79
+ def _internal_error(detail: str = "Internal server error") -> HTTPException:
80
+ """SEC-H-02: log full traceback server-side; return a generic message to the client."""
81
+ logger.exception("stats route error")
82
+ return HTTPException(status_code=500, detail=detail)
83
+
84
+
22
85
  @router.get("/api/stats")
23
- async def get_stats():
86
+ def get_stats():
24
87
  """Get comprehensive system statistics."""
25
88
  try:
26
89
  conn = get_db_connection()
@@ -199,6 +262,9 @@ async def get_stats():
199
262
  importance_dist = cursor.fetchall()
200
263
 
201
264
  db_size = DB_PATH.stat().st_size if DB_PATH.exists() else 0
265
+ ingestion_sources, ingestion_sources_status = (
266
+ _load_ingestion_sources_with_status(cursor, active_profile)
267
+ )
202
268
 
203
269
  if total_graph_nodes > 1:
204
270
  max_edges = (total_graph_nodes * (total_graph_nodes - 1)) / 2
@@ -235,18 +301,21 @@ async def get_stats():
235
301
  if total_graph_nodes > 0 else 0
236
302
  ),
237
303
  },
304
+ "ingestion_sources": ingestion_sources,
305
+ "ingestion_sources_status": ingestion_sources_status,
238
306
  }
239
307
 
240
- except Exception as e:
241
- raise HTTPException(status_code=500, detail=f"Stats error: {str(e)}")
308
+ except Exception:
309
+ raise _internal_error("Stats error")
242
310
 
243
311
 
244
312
  @router.get("/api/timeline")
245
- async def get_timeline(
313
+ def get_timeline(
246
314
  days: int = Query(30, ge=1, le=365),
247
315
  group_by: str = Query("day", pattern="^(day|week|month)$"),
316
+ include_categories: bool = Query(True),
248
317
  ):
249
- """Get temporal view of memory creation with flexible grouping."""
318
+ """Get temporal memory creation; dashboard callers may skip category scans."""
250
319
  try:
251
320
  conn = get_db_connection()
252
321
  conn.row_factory = dict_factory
@@ -280,22 +349,29 @@ async def get_timeline(
280
349
  """, (days, active_profile))
281
350
  timeline = cursor.fetchall()
282
351
 
283
- cursor.execute(f"""
284
- SELECT {date_group} as period, {cat_col} as category, COUNT(*) as count
285
- FROM {table}
286
- WHERE created_at >= datetime('now', '-' || ? || ' days')
287
- AND {cat_col} IS NOT NULL AND {profile_col} = ?
288
- GROUP BY {date_group}, {cat_col} ORDER BY period DESC, count DESC
289
- """, (days, active_profile))
290
- category_trend = cursor.fetchall()
291
-
292
- cursor.execute(f"""
293
- SELECT COUNT(*) as total_memories,
294
- COUNT(DISTINCT {cat_col}) as categories_used
295
- FROM {table}
296
- WHERE created_at >= datetime('now', '-' || ? || ' days') AND {profile_col} = ?
297
- """, (days, active_profile))
298
- period_stats = cursor.fetchone()
352
+ if include_categories:
353
+ cursor.execute(f"""
354
+ SELECT {date_group} as period, {cat_col} as category, COUNT(*) as count
355
+ FROM {table}
356
+ WHERE created_at >= datetime('now', '-' || ? || ' days')
357
+ AND {cat_col} IS NOT NULL AND {profile_col} = ?
358
+ GROUP BY {date_group}, {cat_col} ORDER BY period DESC, count DESC
359
+ """, (days, active_profile))
360
+ category_trend = cursor.fetchall()
361
+
362
+ cursor.execute(f"""
363
+ SELECT COUNT(*) as total_memories,
364
+ COUNT(DISTINCT {cat_col}) as categories_used
365
+ FROM {table}
366
+ WHERE created_at >= datetime('now', '-' || ? || ' days') AND {profile_col} = ?
367
+ """, (days, active_profile))
368
+ period_stats = cursor.fetchone()
369
+ else:
370
+ category_trend = []
371
+ period_stats = {
372
+ "total_memories": sum(int(row["count"] or 0) for row in timeline),
373
+ "categories_used": None,
374
+ }
299
375
 
300
376
  conn.close()
301
377
 
@@ -305,136 +381,5 @@ async def get_timeline(
305
381
  "parameters": {"days": days, "group_by": group_by},
306
382
  }
307
383
 
308
- except Exception as e:
309
- raise HTTPException(status_code=500, detail=f"Timeline error: {str(e)}")
310
-
311
-
312
- @router.get("/api/patterns")
313
- async def get_patterns():
314
- """Get learned patterns."""
315
- try:
316
- conn = get_db_connection()
317
- conn.row_factory = dict_factory
318
- cursor = conn.cursor()
319
- active_profile = get_active_profile()
320
-
321
- # Check for V3 learning tables or V2 identity_patterns
322
- patterns = []
323
- table_name = None
324
- for candidate in ('learned_patterns', 'identity_patterns'):
325
- cursor.execute(
326
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
327
- (candidate,),
328
- )
329
- if cursor.fetchone():
330
- table_name = candidate
331
- break
332
-
333
- if not table_name:
334
- conn.close()
335
- # Fall through to V3.1 behavioral pattern store
336
- try:
337
- from superlocalmemory.learning.behavioral import BehavioralPatternStore
338
- store = BehavioralPatternStore(str(MEMORY_DIR / "learning.db"))
339
- raw = store.get_patterns(profile_id=active_profile)
340
- # v3.4.7: Map all pattern types to frontend categories
341
- type_map = {
342
- "tech_preference": "preference",
343
- "interest": "preference",
344
- "entity_preferences": "preference",
345
- "style": "style",
346
- "fact_type_distribution": "style",
347
- "knowledge_structure": "style",
348
- "terminology": "terminology",
349
- "temporal": "workflow",
350
- "session_activity": "workflow",
351
- "workflow": "workflow",
352
- "co_retrieval_clusters": "workflow",
353
- "channel_performance": "performance",
354
- }
355
- grouped = defaultdict(list)
356
- for p in raw:
357
- meta = p.get("metadata", {})
358
- data = meta # metadata IS the data dict
359
- frontend_key = type_map.get(p.get("pattern_type", ""), "preference")
360
- # Extract human-readable value from data fields
361
- readable_value = (
362
- data.get("value")
363
- or data.get("topic")
364
- or data.get("pattern_key", "")
365
- or p.get("pattern_key", "")
366
- )
367
- readable_key = (
368
- data.get("pattern_key")
369
- or data.get("key")
370
- or p.get("pattern_key", "")
371
- )
372
- grouped[frontend_key].append({
373
- "pattern_type": p.get("pattern_type", ""),
374
- "key": readable_key,
375
- "value": readable_value,
376
- "confidence": p.get("confidence", 0),
377
- "evidence_count": data.get("evidence", p.get("evidence_count", 0)),
378
- })
379
- all_patterns = [p for ps in grouped.values() for p in ps]
380
- confs = [p["confidence"] for p in all_patterns if p.get("confidence")]
381
- return {
382
- "patterns": dict(grouped),
383
- "total_patterns": len(all_patterns),
384
- "pattern_types": list(grouped.keys()),
385
- "confidence_stats": {
386
- "avg": sum(confs) / len(confs) if confs else 0,
387
- "min": min(confs) if confs else 0,
388
- "max": max(confs) if confs else 0,
389
- },
390
- }
391
- except Exception:
392
- return {
393
- "patterns": {}, "total_patterns": 0, "pattern_types": [],
394
- "message": "Pattern learning not initialized.",
395
- }
396
-
397
- if table_name == 'identity_patterns':
398
- cursor.execute("""
399
- SELECT pattern_type, key, value, confidence, evidence_count,
400
- updated_at as last_updated
401
- FROM identity_patterns WHERE profile = ?
402
- ORDER BY confidence DESC, evidence_count DESC
403
- """, (active_profile,))
404
- else:
405
- cursor.execute("""
406
- SELECT pattern_type, key, value, confidence, evidence_count,
407
- last_updated
408
- FROM learned_patterns WHERE is_active = 1
409
- ORDER BY confidence DESC, evidence_count DESC
410
- """)
411
-
412
- patterns = cursor.fetchall()
413
-
414
- for pattern in patterns:
415
- if pattern.get('value'):
416
- try:
417
- pattern['value'] = json.loads(pattern['value'])
418
- except Exception:
419
- pass
420
-
421
- grouped = defaultdict(list)
422
- for pattern in patterns:
423
- grouped[pattern['pattern_type']].append(pattern)
424
-
425
- confidences = [p['confidence'] for p in patterns if p.get('confidence')]
426
- confidence_stats = {
427
- "avg": sum(confidences) / len(confidences) if confidences else 0,
428
- "min": min(confidences) if confidences else 0,
429
- "max": max(confidences) if confidences else 0,
430
- }
431
-
432
- conn.close()
433
-
434
- return {
435
- "patterns": dict(grouped), "total_patterns": len(patterns),
436
- "pattern_types": list(grouped.keys()), "confidence_stats": confidence_stats,
437
- }
438
-
439
- except Exception as e:
440
- return {"patterns": {}, "total_patterns": 0, "error": str(e)}
384
+ except Exception:
385
+ raise _internal_error("Timeline error")
@@ -20,7 +20,7 @@ from pydantic import BaseModel, Field
20
20
 
21
21
  from superlocalmemory.server.route_mutations import authorize_route_mutation
22
22
 
23
- from .helpers import DB_PATH
23
+ from .helpers import DB_PATH, get_active_profile
24
24
 
25
25
  logger = logging.getLogger("superlocalmemory.routes.tiers")
26
26
  router = APIRouter()
@@ -55,9 +55,11 @@ def _validate_profile(profile_id: str) -> str:
55
55
 
56
56
 
57
57
  @router.get("/api/tiers/stats")
58
- async def tier_stats(profile_id: str = "default"):
58
+ async def tier_stats(profile_id: str | None = None):
59
59
  """Get tier distribution stats."""
60
- profile_id = _validate_profile(profile_id)
60
+ # Default to the ACTIVE profile, never the literal "default" — otherwise
61
+ # tier counts reflect the default profile regardless of which is active.
62
+ profile_id = _validate_profile(profile_id or get_active_profile())
61
63
 
62
64
  with _db() as conn:
63
65
  try:
@@ -98,13 +100,13 @@ async def tier_stats(profile_id: str = "default"):
98
100
 
99
101
 
100
102
  @router.post("/api/tiers/evaluate")
101
- async def evaluate_tiers_route(request: Request, profile_id: str = "default"):
103
+ async def evaluate_tiers_route(request: Request, profile_id: str | None = None):
102
104
  """Manually trigger tier evaluation.
103
105
 
104
106
  Uses the shared engine (via lazy import) instead of re-initializing
105
107
  DatabaseManager on every request.
106
108
  """
107
- profile_id = _validate_profile(profile_id)
109
+ profile_id = _validate_profile(profile_id or get_active_profile())
108
110
 
109
111
  try:
110
112
  authorization = authorize_route_mutation(
@@ -137,13 +139,13 @@ async def evaluate_tiers_route(request: Request, profile_id: str = "default"):
137
139
  async def pin_fact_route(
138
140
  body: PinRequest,
139
141
  request: Request,
140
- profile_id: str = "default",
142
+ profile_id: str | None = None,
141
143
  ):
142
144
  """Pin a fact to stay in active tier forever.
143
145
 
144
146
  Validates fact exists in the specified profile before pinning.
145
147
  """
146
- profile_id = _validate_profile(profile_id)
148
+ profile_id = _validate_profile(profile_id or get_active_profile())
147
149
  authorization = authorize_route_mutation(
148
150
  request,
149
151
  operation="update",
@@ -194,7 +196,7 @@ async def pin_fact_route(
194
196
  async def unpin_fact_route(
195
197
  body: PinRequest,
196
198
  request: Request,
197
- profile_id: str = "default",
199
+ profile_id: str | None = None,
198
200
  ):
199
201
  """Unpin a fact, allowing normal tier demotion.
200
202
 
@@ -202,7 +204,7 @@ async def unpin_fact_route(
202
204
  based on access patterns. This is intentional — immediate demotion would
203
205
  surprise the user.
204
206
  """
205
- profile_id = _validate_profile(profile_id)
207
+ profile_id = _validate_profile(profile_id or get_active_profile())
206
208
  authorization = authorize_route_mutation(
207
209
  request,
208
210
  operation="update",
@@ -31,21 +31,11 @@ logger = logging.getLogger(__name__)
31
31
  router = APIRouter(tags=["internal"])
32
32
 
33
33
 
34
- _ALLOWED_ORIGIN_PREFIXES = (
35
- "http://127.0.0.1",
36
- "https://127.0.0.1",
37
- "http://localhost",
38
- "https://localhost",
39
- "http://[::1]",
40
- "https://[::1]",
41
- )
42
-
43
-
44
34
  def _origin_is_loopback(origin: str) -> bool:
45
35
  """Return True iff ``origin`` is absent or a loopback URL."""
46
- if not origin:
47
- return True
48
- return any(origin.startswith(p) for p in _ALLOWED_ORIGIN_PREFIXES)
36
+ from superlocalmemory.server.origin import origin_is_loopback
37
+
38
+ return origin_is_loopback(origin)
49
39
 
50
40
 
51
41
  @router.get("/internal/token")