superlocalmemory 3.7.7 → 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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -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 -2
  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 +96 -12
  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/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -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/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -62,7 +62,14 @@ class TemporalChannel:
62
62
  def __init__(self, db: DatabaseManager) -> None:
63
63
  self._db = db
64
64
 
65
- def search(self, query: str, profile_id: str, top_k: int = 30) -> list[tuple[str, float]]:
65
+ def search(
66
+ self,
67
+ query: str,
68
+ profile_id: str,
69
+ top_k: int = 30,
70
+ include_global: bool | None = None,
71
+ include_shared: bool | None = None,
72
+ ) -> list[tuple[str, float]]:
66
73
  """Search for temporally relevant facts.
67
74
 
68
75
  Two strategies:
@@ -72,7 +79,17 @@ class TemporalChannel:
72
79
 
73
80
  Returns empty only when query has no temporal signal AND no
74
81
  entity-temporal matches.
82
+
83
+ Args:
84
+ include_global: Include global-scope facts. Falls back to the
85
+ instance attribute when not supplied.
86
+ include_shared: Include shared-scope facts. Same fallback.
75
87
  """
88
+ if include_global is None:
89
+ include_global = bool(getattr(self, "include_global", False))
90
+ if include_shared is None:
91
+ include_shared = bool(getattr(self, "include_shared", False))
92
+
76
93
  parser = TemporalParser()
77
94
  dates = parser.extract_dates_from_text(query)
78
95
  query_dt = _parse_iso(dates.get("referenced_date"))
@@ -81,13 +98,18 @@ class TemporalChannel:
81
98
 
82
99
  # Strategy 1: Entity-temporal metadata search
83
100
  # "When did Alice...?" → find all temporal events for Alice
84
- entity_results = self._entity_temporal_search(query, profile_id)
101
+ entity_results = self._entity_temporal_search(
102
+ query, profile_id,
103
+ include_global=include_global, include_shared=include_shared,
104
+ )
85
105
 
86
106
  # Strategy 2: Date proximity search
87
107
  if query_dt is None and not entity_results:
88
108
  return []
89
109
 
90
- events = self._load_events(profile_id)
110
+ events = self._load_events(
111
+ profile_id, include_global=include_global, include_shared=include_shared,
112
+ )
91
113
  scored: dict[str, float] = {}
92
114
 
93
115
  # Include entity-temporal results with high base score
@@ -124,13 +146,21 @@ class TemporalChannel:
124
146
  return results[:top_k]
125
147
 
126
148
  def _entity_temporal_search(
127
- self, query: str, profile_id: str,
149
+ self,
150
+ query: str,
151
+ profile_id: str,
152
+ include_global: bool | None = None,
153
+ include_shared: bool | None = None,
128
154
  ) -> list[tuple[str, float]]:
129
155
  """Metadata-first: find temporal events for entities mentioned in query.
130
156
 
131
157
  "When did Alice do X?" → SQL filter by entity_id for Alice → return
132
158
  all temporal facts about Alice. High precision for entity+time queries.
133
159
  """
160
+ if include_global is None:
161
+ include_global = bool(getattr(self, "include_global", False))
162
+ if include_shared is None:
163
+ include_shared = bool(getattr(self, "include_shared", False))
134
164
  import re
135
165
  _PROPER_RE = re.compile(r"\b([A-Z][a-z]+)\b")
136
166
  names = [m.group(1) for m in _PROPER_RE.finditer(query)]
@@ -151,8 +181,8 @@ class TemporalChannel:
151
181
  seen: set[str] = set()
152
182
  where, params = _scope_where(
153
183
  profile_id,
154
- include_global=bool(getattr(self, "include_global", False)),
155
- include_shared=bool(getattr(self, "include_shared", False)),
184
+ include_global=include_global,
185
+ include_shared=include_shared,
156
186
  prefix="af",
157
187
  )
158
188
 
@@ -179,11 +209,20 @@ class TemporalChannel:
179
209
 
180
210
  return results
181
211
 
182
- def _load_events(self, profile_id: str) -> list[dict]:
212
+ def _load_events(
213
+ self,
214
+ profile_id: str,
215
+ include_global: bool | None = None,
216
+ include_shared: bool | None = None,
217
+ ) -> list[dict]:
218
+ if include_global is None:
219
+ include_global = bool(getattr(self, "include_global", False))
220
+ if include_shared is None:
221
+ include_shared = bool(getattr(self, "include_shared", False))
183
222
  where, params = _scope_where(
184
223
  profile_id,
185
- include_global=bool(getattr(self, "include_global", False)),
186
- include_shared=bool(getattr(self, "include_shared", False)),
224
+ include_global=include_global,
225
+ include_shared=include_shared,
187
226
  prefix="af",
188
227
  )
189
228
  rows = self._db.execute(
@@ -0,0 +1,102 @@
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 | https://varunpratap.com
4
+
5
+ """Temporal-context injection helpers (Phase 4, T-inject).
6
+
7
+ LLMs have no innate sense of time — a recalled fact reads the same whether it
8
+ was stored an hour ago or two years ago. These pure helpers give every recalled
9
+ memory a human-relative age label and give the whole result set a "temporal
10
+ frame" header anchoring it to *now*, so the model can weigh recency and treat
11
+ aged facts as possibly stale.
12
+
13
+ Reuses ``time_window.parse_timestamp`` for tolerant timestamp parsing (SQLite
14
+ space form, ISO ``T``/``Z``, date-only) — comparisons are always on parsed
15
+ datetimes, never strings.
16
+
17
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
18
+ License: AGPL-3.0-or-later
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from datetime import datetime, timezone
24
+ from typing import Iterable
25
+
26
+ from superlocalmemory.retrieval.time_window import parse_timestamp
27
+
28
+ __all__ = ["relative_age", "temporal_frame"]
29
+
30
+
31
+ def _plural(n: int, unit: str) -> str:
32
+ return f"{n} {unit}" + ("" if n == 1 else "s")
33
+
34
+
35
+ def relative_age(timestamp: str | None, now: datetime | None = None) -> str:
36
+ """Human-relative age of ``timestamp`` vs now, e.g. "3 days ago".
37
+
38
+ Returns "" when the timestamp is missing/unparseable. Future timestamps
39
+ (e.g. a referenced event date ahead of now) read as "in N …".
40
+ """
41
+ dt = parse_timestamp(timestamp)
42
+ if dt is None:
43
+ return ""
44
+ _now = now or datetime.now(timezone.utc)
45
+ if _now.tzinfo is None:
46
+ _now = _now.replace(tzinfo=timezone.utc)
47
+ secs = (_now - dt).total_seconds()
48
+ future = secs < 0
49
+ secs = abs(secs)
50
+
51
+ if secs < 45:
52
+ return "just now"
53
+ minutes = secs / 60.0
54
+ hours = minutes / 60.0
55
+ days = hours / 24.0
56
+ if minutes < 45:
57
+ phrase = _plural(round(minutes), "minute")
58
+ elif hours < 24:
59
+ phrase = _plural(round(hours), "hour")
60
+ elif days < 14:
61
+ phrase = _plural(round(days), "day")
62
+ elif days < 60:
63
+ phrase = _plural(round(days / 7.0), "week")
64
+ elif days < 365:
65
+ phrase = _plural(round(days / 30.0), "month")
66
+ else:
67
+ phrase = _plural(round(days / 365.0), "year")
68
+ return f"in {phrase}" if future else f"{phrase} ago"
69
+
70
+
71
+ def temporal_frame(
72
+ timestamps: Iterable[str | None],
73
+ now: datetime | None = None,
74
+ ) -> str:
75
+ """A one-line "now" anchor + age span for a set of recalled timestamps.
76
+
77
+ Example: ``"Now: 2026-07-22T12:00:00+00:00. Recalled memories span 2 years
78
+ ago → just now. Treat undated or aged facts as possibly stale."``
79
+
80
+ With no dated timestamps, returns just the now-anchor + an undated note.
81
+ """
82
+ _now = now or datetime.now(timezone.utc)
83
+ if _now.tzinfo is None:
84
+ _now = _now.replace(tzinfo=timezone.utc)
85
+ now_iso = _now.replace(microsecond=0).isoformat()
86
+
87
+ dts = [d for d in (parse_timestamp(t) for t in timestamps) if d is not None]
88
+ if not dts:
89
+ return f"Now: {now_iso}. Recalled memories are undated."
90
+
91
+ oldest = min(dts)
92
+ newest = max(dts)
93
+ span = (
94
+ relative_age(oldest.isoformat(), _now)
95
+ if oldest == newest
96
+ else f"{relative_age(oldest.isoformat(), _now)} → "
97
+ f"{relative_age(newest.isoformat(), _now)}"
98
+ )
99
+ return (
100
+ f"Now: {now_iso}. Recalled memories span {span}. "
101
+ f"Treat undated or aged facts as possibly stale."
102
+ )
@@ -0,0 +1,135 @@
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 | https://varunpratap.com
4
+
5
+ """Bi-temporal validity filter for the retrieval pipeline (Phase 4, T1).
6
+
7
+ Post-retrieval filter for *system-invalidated* facts — a fact whose temporal
8
+ record has ``system_expired_at`` set was superseded/contradicted by a newer
9
+ fact (see ``invalidate_fact_temporal`` / conflict-resolution supersession).
10
+
11
+ P5-INT-01 (non-destructive supersession): such a fact is DEMOTED, not hidden.
12
+ Its per-channel score is multiplied by ``superseded_demotion_factor`` (default
13
+ 0.25) and the channel lists are re-sorted, so currently-valid facts rank above
14
+ it — but nothing valid silently vanishes. This is the Mem0-2026 design that
15
+ wins long-term-memory benchmarks: keep every fact recallable and let
16
+ retrieval-time recency resolve conflicts, rather than destructively deleting on
17
+ a write-time contradiction guess (which over-fires: two complementary facts
18
+ about the same entity diverge past the coboundary threshold and one would be
19
+ wrongly hidden). A factor of 0.0 restores the legacy hide behaviour (a demoted
20
+ score of 0 is gated out by the evidence floor).
21
+
22
+ The filter runs on the per-channel candidate dict BEFORE RRF fusion, so fused
23
+ ranks reflect the demotion. It queries validity only for the bounded candidate
24
+ set (never the full ``get_valid_facts`` set) — an indexed, O(candidates) lookup
25
+ on the hot path, no full-table scan.
26
+
27
+ Pure SQL, no LLM → safe in every mode including Mode A. A no-op when nothing is
28
+ invalidated and when config.enabled is False.
29
+
30
+ Integrates with ChannelRegistry.register_filter() using the FilterFn signature:
31
+ (all_channel_results, profile_id, context) -> filtered_results
32
+
33
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
34
+ License: AGPL-3.0-or-later
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import logging
40
+ from typing import TYPE_CHECKING, Any
41
+
42
+ if TYPE_CHECKING:
43
+ from superlocalmemory.core.config import TemporalValidatorConfig
44
+ from superlocalmemory.retrieval.channel_registry import ChannelRegistry
45
+ from superlocalmemory.storage.database import DatabaseManager
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class TemporalValidityFilter:
51
+ """Demotes system-invalidated (superseded) facts in retrieval candidates."""
52
+
53
+ __slots__ = ("_db", "_demotion_factor")
54
+
55
+ def __init__(self, db: DatabaseManager, demotion_factor: float = 0.25) -> None:
56
+ self._db = db
57
+ # Clamp to [0, 1]. 0.0 = legacy hide (evidence floor drops zero-score
58
+ # facts); 1.0 = no demotion.
59
+ self._demotion_factor = max(0.0, min(1.0, float(demotion_factor)))
60
+
61
+ def filter(
62
+ self,
63
+ all_results: dict[str, list[tuple[str, float]]],
64
+ profile_id: str,
65
+ context: Any,
66
+ ) -> dict[str, list[tuple[str, float]]]:
67
+ """Demote superseded fact_ids in every channel's candidate list.
68
+
69
+ Matches FilterFn signature from channel_registry.py.
70
+
71
+ Args:
72
+ all_results: Channel name -> [(fact_id, score)] dict.
73
+ profile_id: Current profile.
74
+ context: Optional context (unused).
75
+
76
+ Returns:
77
+ A new dict where system-invalidated facts keep their channel
78
+ presence but have their score scaled by the demotion factor and the
79
+ channel lists re-sorted (so valid facts rank above them). Inputs are
80
+ never mutated (immutability). Unchanged when nothing is invalidated.
81
+ """
82
+ # Collect all unique candidate fact_ids across every channel.
83
+ all_fact_ids: set[str] = set()
84
+ for channel_results in all_results.values():
85
+ for fact_id, _ in channel_results:
86
+ all_fact_ids.add(fact_id)
87
+
88
+ if not all_fact_ids:
89
+ return all_results
90
+
91
+ try:
92
+ invalid = self._db.get_invalidated_fact_ids(
93
+ list(all_fact_ids), profile_id,
94
+ )
95
+ except Exception as exc:
96
+ # Fail-open: a validity-lookup error must never break retrieval.
97
+ logger.warning("Temporal validity lookup failed: %s", exc)
98
+ return all_results
99
+
100
+ if not invalid:
101
+ return all_results
102
+
103
+ factor = self._demotion_factor
104
+ demoted: dict[str, list[tuple[str, float]]] = {}
105
+ for channel_name, channel_results in all_results.items():
106
+ new_list = [
107
+ (fact_id, score * factor if fact_id in invalid else score)
108
+ for fact_id, score in channel_results
109
+ ]
110
+ # Re-sort descending so demoted (superseded) facts fall below
111
+ # currently-valid facts in this channel's rank order.
112
+ new_list.sort(key=lambda pair: pair[1], reverse=True)
113
+ demoted[channel_name] = new_list
114
+ return demoted
115
+
116
+
117
+ def register_temporal_validity_filter(
118
+ registry: ChannelRegistry,
119
+ db: DatabaseManager,
120
+ config: TemporalValidatorConfig,
121
+ ) -> None:
122
+ """Register the bi-temporal validity filter into the channel registry.
123
+
124
+ Does nothing if config.enabled is False.
125
+
126
+ Args:
127
+ registry: Channel registry to register with.
128
+ db: Database manager for validity queries.
129
+ config: Temporal-validator configuration.
130
+ """
131
+ if not getattr(config, "enabled", True):
132
+ return
133
+ factor = getattr(config, "superseded_demotion_factor", 0.25)
134
+ f = TemporalValidityFilter(db, demotion_factor=factor)
135
+ registry.register_filter(f.filter)
@@ -0,0 +1,181 @@
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 | https://varunpratap.com
4
+
5
+ """Time-window parsing + membership for recall (Phase 4, T-window).
6
+
7
+ Pure, dependency-free helpers so recall() can prune candidates to an
8
+ event-time range. Two window forms are accepted:
9
+
10
+ * relative string — ``"1h" | "24h" | "7d" | "30d" | "90d" | "1y"`` etc.
11
+ (``<int><unit>`` where unit is h/d/w/m/y; m≈30d, y≈365d) → ``[now-Δ, now]``.
12
+ * explicit range — ``(start_iso, end_iso)`` two-tuple of timestamps.
13
+
14
+ Timestamps are parsed tolerantly: SQLite ``datetime('now')`` form
15
+ (``"YYYY-MM-DD HH:MM:SS"``), ISO-8601 with ``T`` and/or trailing ``Z``, and
16
+ date-only ``"YYYY-MM-DD"`` are all accepted. Comparing the *strings* would be
17
+ wrong (a space sorts before ``T``), so everything is parsed to tz-aware UTC
18
+ datetimes before comparison — never lexicographic.
19
+
20
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
21
+ License: AGPL-3.0-or-later
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from datetime import datetime, timedelta, timezone
28
+
29
+ __all__ = [
30
+ "parse_timestamp",
31
+ "parse_window",
32
+ "in_window",
33
+ "infer_window_from_query",
34
+ ]
35
+
36
+ _REL = re.compile(r"^\s*(\d+)\s*([hdwmy])\s*$", re.IGNORECASE)
37
+
38
+ # Hours per unit. Month and year are documented approximations.
39
+ _UNIT_HOURS: dict[str, int] = {
40
+ "h": 1,
41
+ "d": 24,
42
+ "w": 24 * 7,
43
+ "m": 24 * 30,
44
+ "y": 24 * 365,
45
+ }
46
+
47
+
48
+ def parse_timestamp(value: str | None) -> datetime | None:
49
+ """Parse a stored timestamp into a tz-aware UTC datetime, or None.
50
+
51
+ Accepts SQLite ``datetime('now')`` (space-separated), ISO-8601 (``T`` and
52
+ optional ``Z``), and date-only strings. Naive values are assumed UTC.
53
+ """
54
+ if not value or not isinstance(value, str):
55
+ return None
56
+ text = value.strip()
57
+ if not text:
58
+ return None
59
+ if text.endswith(("Z", "z")):
60
+ text = text[:-1] + "+00:00"
61
+ dt: datetime | None = None
62
+ try:
63
+ dt = datetime.fromisoformat(text)
64
+ except ValueError:
65
+ # Fall back to a date-only prefix (e.g. "2026-03-15 ...").
66
+ try:
67
+ dt = datetime.fromisoformat(text[:10])
68
+ except ValueError:
69
+ return None
70
+ if dt.tzinfo is None:
71
+ dt = dt.replace(tzinfo=timezone.utc)
72
+ return dt.astimezone(timezone.utc)
73
+
74
+
75
+ def parse_window(
76
+ window: str | tuple[str, str] | list | None,
77
+ now: datetime | None = None,
78
+ ) -> tuple[datetime, datetime] | None:
79
+ """Resolve a window spec to a ``(start, end)`` UTC datetime pair, or None.
80
+
81
+ Returns None when the spec is None or unparseable (caller then applies no
82
+ time filter — additive, fail-open).
83
+ """
84
+ if window is None:
85
+ return None
86
+ _now = now or datetime.now(timezone.utc)
87
+ if _now.tzinfo is None:
88
+ _now = _now.replace(tzinfo=timezone.utc)
89
+
90
+ # Explicit (start, end) range.
91
+ if isinstance(window, (tuple, list)):
92
+ if len(window) != 2:
93
+ return None
94
+ start = parse_timestamp(window[0])
95
+ end = parse_timestamp(window[1])
96
+ if start is None or end is None:
97
+ return None
98
+ return (start, end) if start <= end else (end, start)
99
+
100
+ # String forms: relative "<int><unit>" or an explicit range written as
101
+ # "start..end" / "start,end" (so it survives URL params, JSON, and CLI args
102
+ # as a single value — no tuple serialization needed on the wire).
103
+ if isinstance(window, str):
104
+ m = _REL.match(window)
105
+ if m:
106
+ n = int(m.group(1))
107
+ unit = m.group(2).lower()
108
+ hours = n * _UNIT_HOURS[unit]
109
+ return (_now - timedelta(hours=hours), _now)
110
+ for sep in ("..", ","):
111
+ if sep in window:
112
+ left, _, right = window.partition(sep)
113
+ start = parse_timestamp(left)
114
+ end = parse_timestamp(right)
115
+ if start is None or end is None:
116
+ return None
117
+ return (start, end) if start <= end else (end, start)
118
+
119
+ return None
120
+
121
+
122
+ # Natural-language temporal-scope patterns → relative window spec. Ordered:
123
+ # more specific ("last 3 weeks") is matched before generic ("last week").
124
+ _UNIT_TO_SPEC = {"day": "d", "week": "w", "month": "m", "year": "y"}
125
+ _QUERY_N_UNIT = re.compile(
126
+ r"\b(?:last|past|previous|prior)\s+(\d{1,3})\s+(day|week|month|year)s?\b",
127
+ re.IGNORECASE,
128
+ )
129
+ _QUERY_PHRASES: tuple[tuple[re.Pattern[str], str], ...] = (
130
+ (re.compile(r"\btoday\b", re.IGNORECASE), "1d"),
131
+ (re.compile(r"\byesterday\b", re.IGNORECASE), "2d"),
132
+ (re.compile(r"\b(?:this|last|past|previous)\s+week\b", re.IGNORECASE), "7d"),
133
+ (re.compile(r"\b(?:this|last|past|previous)\s+month\b", re.IGNORECASE), "30d"),
134
+ (re.compile(r"\b(?:this|last|past|previous)\s+year\b", re.IGNORECASE), "1y"),
135
+ (re.compile(r"\b(?:recent|recently|lately)\b", re.IGNORECASE), "30d"),
136
+ )
137
+
138
+
139
+ def infer_window_from_query(query: str | None) -> str | None:
140
+ """Infer a relative time window from natural-language scope in a query.
141
+
142
+ Recognises a small, unambiguous set of temporal-scope phrases ("yesterday",
143
+ "last week", "past 3 months", "recently") and maps them to a relative
144
+ window spec ("2d", "7d", "3m", …) that ``parse_window`` understands. Returns
145
+ None when no clear temporal scope is present, so recall applies no window.
146
+
147
+ Deliberately conservative — only fires on explicit scope words, never on
148
+ bare content — so it augments, never surprises. Callers use it only when the
149
+ user did not pass an explicit window.
150
+ """
151
+ if not query or not isinstance(query, str):
152
+ return None
153
+ m = _QUERY_N_UNIT.search(query)
154
+ if m:
155
+ n = int(m.group(1))
156
+ unit = _UNIT_TO_SPEC.get(m.group(2).lower())
157
+ if unit and n > 0:
158
+ return f"{n}{unit}"
159
+ for pattern, spec in _QUERY_PHRASES:
160
+ if pattern.search(query):
161
+ return spec
162
+ return None
163
+
164
+
165
+ def in_window(
166
+ event_time: str | None,
167
+ bounds: tuple[datetime, datetime] | None,
168
+ ) -> bool:
169
+ """True if ``event_time`` falls within ``bounds`` (inclusive).
170
+
171
+ No bounds → always True (no filtering). An unparseable/missing event time
172
+ is excluded (conservative: a windowed query returns only datable facts in
173
+ range).
174
+ """
175
+ if bounds is None:
176
+ return True
177
+ dt = parse_timestamp(event_time)
178
+ if dt is None:
179
+ return False
180
+ start, end = bounds
181
+ return start <= dt <= end
@@ -6,6 +6,23 @@
6
6
  SuperLocalMemory V3 - FastAPI API Server
7
7
  Provides REST endpoints for memory visualization and exploration.
8
8
  Uses V3 MemoryEngine for all operations.
9
+
10
+ v3.7.8 (WS3 F4): ``create_app()`` in this module is a standalone/legacy app
11
+ factory. The running daemon serves ``superlocalmemory.server.unified_daemon:
12
+ create_app`` (see ``unified_daemon.py``'s uvicorn config); THIS factory is
13
+ reachable only via direct ``python -m superlocalmemory.server.api`` /
14
+ programmatic use and the tests that exercise it directly
15
+ (``tests/test_api/test_api_lifespan_contract.py``,
16
+ ``tests/test_security/test_rate_limit_e2e.py``). Its ``auth_middleware``
17
+ below intentionally keeps the older, simpler ``check_api_key``-only gate
18
+ (unconditional per-write check, no daemon-capability / install-token
19
+ identity layer) because it predates and is independent of the unified
20
+ daemon's richer ``write_identity.require_http_mutation_actor`` boundary and
21
+ the ``SLM_REQUIRE_API_KEY_LOOPBACK`` opt-in (see
22
+ ``infra/auth_middleware.py``). Do not treat this module's auth posture as
23
+ the production write boundary -- that is ``unified_daemon.py``'s
24
+ ``auth_middleware``. ``UI_DIR`` below is still imported by
25
+ ``unified_daemon.py`` and must not be removed.
9
26
  """
10
27
 
11
28
  import json
@@ -31,10 +48,10 @@ logger = logging.getLogger("superlocalmemory.api_server")
31
48
  # V3 paths
32
49
  MEMORY_DIR = DynamicStatePath()
33
50
  DB_PATH = DynamicStatePath("memory.db")
34
- # V3.3.21: UI shipped inside the package for pip/npm installs.
35
- _PKG_UI = Path(__file__).resolve().parent.parent / "ui"
36
- _REPO_UI = Path(__file__).resolve().parent.parent.parent.parent / "ui"
37
- UI_DIR = _PKG_UI if (_PKG_UI / "index.html").exists() else _REPO_UI
51
+ # V3.3.21+: the dashboard ships inside the package (superlocalmemory/ui) for
52
+ # every install path. The legacy repo-root ui/ dev fallback was retired in
53
+ # v3.8.0 when that copy was deleted; the packaged directory is authoritative.
54
+ UI_DIR = Path(__file__).resolve().parent.parent / "ui"
38
55
 
39
56
 
40
57
  # ============================================================================