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.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- 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(
|
|
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(
|
|
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(
|
|
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,
|
|
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=
|
|
155
|
-
include_shared=
|
|
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(
|
|
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=
|
|
186
|
-
include_shared=
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
UI_DIR =
|
|
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
|
# ============================================================================
|