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
@@ -124,15 +124,36 @@ def run_health_tick(engine, state: RecallHealth, *, probe: str = DEFAULT_PROBE,
124
124
 
125
125
  Mutates and returns ``state``. Never raises — a timed-out / failing recall
126
126
  marks the path unhealthy instead of propagating.
127
+
128
+ Uses ``operation_nowait()`` when a runtime is supplied so that a pending
129
+ profile switch is not blocked by the health-probe recall. If a transition
130
+ is in progress the tick is skipped entirely — the next scheduled tick will
131
+ re-warm once the switch has committed.
127
132
  """
128
133
  state.checks += 1
129
134
 
130
135
  # Tier 1: re-warm. A real full-fusion recall keeps the graph page cache hot
131
136
  # and the embedder resident.
137
+ # Cooperative preemption: use operation_nowait() so a pending profile switch
138
+ # is not held hostage by a slow full-fusion recall (2–10s with fast=False).
132
139
  try:
133
- lease = runtime.operation() if runtime is not None else nullcontext()
134
- with lease:
135
- resp = engine.recall(probe, limit=3, fast=False)
140
+ if runtime is not None:
141
+ lease = runtime.operation_nowait()
142
+ else:
143
+ lease = nullcontext()
144
+ with lease as _snap:
145
+ if runtime is not None and _snap is None:
146
+ # A profile transition is in progress — skip this tick so we
147
+ # do not hold the drain window. Health state is unchanged;
148
+ # the next tick fires after the switch commits.
149
+ log.debug(
150
+ "recall-health: tick skipped — profile transition in progress"
151
+ )
152
+ return state
153
+ # fast=True: a health probe must release its operation lease well
154
+ # within the 5s profile-switch drain window (fast=False is 2-10s and
155
+ # would make every profile switch time out while a tick is in flight).
156
+ resp = engine.recall(probe, limit=3, fast=True)
136
157
  except Exception as exc:
137
158
  state.healthy = False
138
159
  state.consecutive_failures += 1
@@ -24,6 +24,7 @@ import re
24
24
  from typing import Any
25
25
 
26
26
  from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
27
+ from superlocalmemory.retrieval.temporal_frame import relative_age, temporal_frame
27
28
 
28
29
 
29
30
  # ---------------------------------------------------------------------------
@@ -190,9 +191,13 @@ def serialize_recall_response(
190
191
  is the evidence-floor signal lifted from the response (additive).
191
192
  """
192
193
  memory_map = memory_map or {}
194
+ # T-inject: one shared "now" so every result's age label is consistent.
195
+ from datetime import datetime as _dt, timezone as _tz
196
+ _now = _dt.now(_tz.utc)
193
197
  raw: list[dict] = []
194
198
  for r in (response.results or [])[:limit]:
195
199
  fact = r.fact
200
+ _created = getattr(fact, "created_at", "") or ""
196
201
  fact_type = getattr(fact, "fact_type", None)
197
202
  lifecycle = getattr(fact, "lifecycle", None)
198
203
  raw.append({
@@ -225,7 +230,10 @@ def serialize_recall_response(
225
230
  if lifecycle is not None and hasattr(lifecycle, "value")
226
231
  else (lifecycle or ""),
227
232
  "access_count": getattr(fact, "access_count", 0),
228
- "created_at": getattr(fact, "created_at", "") or "",
233
+ "created_at": _created,
234
+ # T-inject: human-relative age so consumers (and the LLM) can
235
+ # weigh recency without doing date math. "" when undated.
236
+ "age_label": relative_age(_created, _now),
229
237
  "evidence_chain": list(getattr(r, "evidence_chain", []) or []),
230
238
  })
231
239
 
@@ -241,6 +249,12 @@ def serialize_recall_response(
241
249
 
242
250
  def recall_response_metadata(response: Any) -> dict:
243
251
  """Return Score Contract v2 response metadata for transport envelopes."""
252
+ # T-inject: a one-line temporal frame anchoring the result set to "now"
253
+ # and its age span, so time-blind LLMs get an explicit recency signal.
254
+ _timestamps = [
255
+ getattr(getattr(r, "fact", None), "created_at", "") or ""
256
+ for r in (getattr(response, "results", None) or [])
257
+ ]
244
258
  return {
245
259
  "score_contract_version": getattr(response, "score_contract_version", "2"),
246
260
  "calibration_status": getattr(response, "calibration_status", "uncalibrated"),
@@ -248,4 +262,8 @@ def recall_response_metadata(response: Any) -> dict:
248
262
  "answer_confidence": getattr(response, "answer_confidence", None),
249
263
  "abstained": bool(getattr(response, "abstained", False)),
250
264
  "abstention_reason": getattr(response, "abstention_reason", None),
265
+ "temporal_frame": temporal_frame(_timestamps),
266
+ # Q2b: thematic community summary (pure pass-through; computed upstream
267
+ # in the engine where DB access is available). None on most recalls.
268
+ "thematic_context": getattr(response, "community_context", None),
251
269
  }
@@ -0,0 +1,115 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later — see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com
4
+
5
+ """Progressive-abstraction read API (Wave Q3).
6
+
7
+ Exposes the abstraction hierarchy so the dashboard can browse it and drill
8
+ down to source atoms:
9
+
10
+ GET /api/v3/abstraction/persona — the per-profile persona roll-up
11
+ GET /api/v3/abstraction/communities — community summaries (Q2)
12
+ GET /api/v3/abstraction/sources — drill-down (node -> source atoms)
13
+
14
+ Read-only, profile-scoped (Rule 01), direct sqlite3 (Rule 06). All handlers
15
+ fail-soft: a missing DB or table returns an empty payload, never a 500.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import sqlite3
22
+ from typing import Any
23
+
24
+ from fastapi import APIRouter, Query
25
+ from fastapi.responses import JSONResponse
26
+
27
+ from superlocalmemory.server.routes.helpers import DB_PATH, get_active_profile
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ router = APIRouter(prefix="/api/v3/abstraction", tags=["abstraction"])
32
+
33
+
34
+ class _ReadDB:
35
+ """Adapt a raw sqlite3 connection to the .execute(...) -> list contract
36
+ the read-only builder methods expect (matches DatabaseManager.execute)."""
37
+
38
+ def __init__(self, conn: sqlite3.Connection) -> None:
39
+ self._conn = conn
40
+
41
+ def execute(self, sql: str, params: tuple = ()) -> list:
42
+ return self._conn.execute(sql, params).fetchall()
43
+
44
+
45
+ def _conn() -> sqlite3.Connection | None:
46
+ if not DB_PATH.exists():
47
+ return None
48
+ conn = sqlite3.connect(str(DB_PATH))
49
+ conn.row_factory = sqlite3.Row
50
+ return conn
51
+
52
+
53
+ @router.get("/persona")
54
+ def get_persona(profile: str = Query("")) -> JSONResponse:
55
+ pid = profile or get_active_profile()
56
+ conn = _conn()
57
+ if conn is None:
58
+ return JSONResponse({"profile": pid, "persona": None})
59
+ try:
60
+ from superlocalmemory.core.progressive_abstraction import ProgressiveAbstraction
61
+
62
+ persona = ProgressiveAbstraction(_ReadDB(conn)).get_persona(pid)
63
+ return JSONResponse({"profile": pid, "persona": persona})
64
+ except Exception as exc: # pragma: no cover - defensive
65
+ logger.debug("persona read failed: %s", exc)
66
+ return JSONResponse({"profile": pid, "persona": None})
67
+ finally:
68
+ conn.close()
69
+
70
+
71
+ @router.get("/communities")
72
+ def get_communities(profile: str = Query("")) -> JSONResponse:
73
+ pid = profile or get_active_profile()
74
+ conn = _conn()
75
+ if conn is None:
76
+ return JSONResponse({"profile": pid, "communities": []})
77
+ try:
78
+ from superlocalmemory.core.community_summary import CommunitySummaryBuilder
79
+
80
+ summaries = CommunitySummaryBuilder(_ReadDB(conn)).get_summaries(pid)
81
+ return JSONResponse({"profile": pid, "communities": summaries})
82
+ except Exception as exc: # pragma: no cover - defensive
83
+ logger.debug("communities read failed: %s", exc)
84
+ return JSONResponse({"profile": pid, "communities": []})
85
+ finally:
86
+ conn.close()
87
+
88
+
89
+ @router.get("/sources")
90
+ def get_sources(
91
+ profile: str = Query(""), node: str = Query("persona"),
92
+ ) -> JSONResponse:
93
+ pid = profile or get_active_profile()
94
+ conn = _conn()
95
+ empty: dict[str, Any] = {
96
+ "node_id": node, "node_type": "unknown", "communities": [], "fact_ids": [],
97
+ }
98
+ if conn is None:
99
+ return JSONResponse({"profile": pid, "sources": empty})
100
+ try:
101
+ from superlocalmemory.core.progressive_abstraction import ProgressiveAbstraction
102
+
103
+ node_val: Any = node
104
+ if node != "persona":
105
+ try:
106
+ node_val = int(node)
107
+ except (ValueError, TypeError):
108
+ node_val = node
109
+ sources = ProgressiveAbstraction(_ReadDB(conn)).get_sources(pid, node_val)
110
+ return JSONResponse({"profile": pid, "sources": sources})
111
+ except Exception as exc: # pragma: no cover - defensive
112
+ logger.debug("sources read failed: %s", exc)
113
+ return JSONResponse({"profile": pid, "sources": empty})
114
+ finally:
115
+ conn.close()
@@ -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")