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.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- 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/rules/AGENTS.md +57 -18
- 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-graph/SKILL.md +17 -3
- 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-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- 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 +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- 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/cli/version_banner.py +17 -3
- 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 +23 -59
- 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 +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- 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/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- 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 +7 -3
- 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 +186 -29
- 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/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- 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/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- 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 +263 -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 +32 -79
- 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 +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- 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 +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- 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 +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- 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 +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- 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 +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- 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 +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- 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 +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- 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 +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -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 +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -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 +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -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 +17 -3
- 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/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -19,6 +19,18 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
router = APIRouter(prefix="/api/v3", tags=["v3"])
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _internal_error() -> JSONResponse:
|
|
23
|
+
"""SEC-H-02: log the full traceback server-side, return a generic message.
|
|
24
|
+
|
|
25
|
+
Returning ``str(e)`` to the client leaked DB schema (column/constraint
|
|
26
|
+
names), the data-directory filesystem path, and — for config routes — could
|
|
27
|
+
surface LLM config internals. Call this only from inside an ``except`` block
|
|
28
|
+
so ``logger.exception`` captures the active traceback.
|
|
29
|
+
"""
|
|
30
|
+
logger.exception("v3_api request failed")
|
|
31
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
async def _apply_runtime_config(request: Request, config, *, mode_change: bool) -> None:
|
|
23
35
|
"""Persist and hot-swap config only after the daemon transition succeeds."""
|
|
24
36
|
import asyncio
|
|
@@ -40,6 +52,42 @@ async def _apply_runtime_config(request: Request, config, *, mode_change: bool)
|
|
|
40
52
|
authorization.complete()
|
|
41
53
|
|
|
42
54
|
|
|
55
|
+
def _require_manage(request: Request) -> None:
|
|
56
|
+
"""Use one explicit authorization boundary for dashboard configuration."""
|
|
57
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
58
|
+
|
|
59
|
+
require_manage(request)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolve_profile(request: Request, requested: str = "") -> str:
|
|
63
|
+
"""Resolve a profile override and enforce READ on its actual target."""
|
|
64
|
+
from superlocalmemory.access.rbac import Permission
|
|
65
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
66
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
67
|
+
|
|
68
|
+
if requested and not isinstance(requested, str):
|
|
69
|
+
raise HTTPException(status_code=422, detail="profile must be a string")
|
|
70
|
+
profile = requested.strip() if requested else get_active_profile()
|
|
71
|
+
require_permission(request, Permission.READ, profile=profile)
|
|
72
|
+
return profile
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_mutation_profile(requested: object) -> str:
|
|
76
|
+
"""Resolve a body-supplied profile without silently coercing bad input."""
|
|
77
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
78
|
+
|
|
79
|
+
if requested is not None and not isinstance(requested, str):
|
|
80
|
+
raise HTTPException(status_code=422, detail="profile must be a string")
|
|
81
|
+
return requested.strip() if requested and requested.strip() else get_active_profile()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _require_manage_for_profile(request: Request, profile: str) -> None:
|
|
85
|
+
"""Enforce MANAGE on the resolved mutation target before any side effect."""
|
|
86
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
87
|
+
|
|
88
|
+
require_manage(request, profile=profile)
|
|
89
|
+
|
|
90
|
+
|
|
43
91
|
# ── Dashboard ────────────────────────────────────────────────
|
|
44
92
|
|
|
45
93
|
@router.get("/dashboard")
|
|
@@ -99,7 +147,7 @@ async def dashboard(request: Request):
|
|
|
99
147
|
"version": SLM_VERSION,
|
|
100
148
|
}
|
|
101
149
|
except Exception as e:
|
|
102
|
-
return
|
|
150
|
+
return _internal_error()
|
|
103
151
|
|
|
104
152
|
|
|
105
153
|
# ── Mode ─────────────────────────────────────────────────────
|
|
@@ -109,26 +157,36 @@ async def get_mode(request: Request):
|
|
|
109
157
|
"""Get current mode, provider, model — single source of truth for UI."""
|
|
110
158
|
try:
|
|
111
159
|
from superlocalmemory.core.config import SLMConfig
|
|
160
|
+
from urllib.parse import urlparse
|
|
112
161
|
config = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
113
162
|
current = config.mode.value
|
|
163
|
+
# SEC-L-01: expose only the endpoint HOST, not the full URL. A full
|
|
164
|
+
# api_base (e.g. an internal Azure OpenAI deployment path) leaks
|
|
165
|
+
# infrastructure topology to non-admin (viewer) users.
|
|
166
|
+
_api_base = config.llm.api_base or ""
|
|
167
|
+
_endpoint_host = urlparse(_api_base).netloc if _api_base else ""
|
|
114
168
|
return {
|
|
115
169
|
"mode": current,
|
|
116
170
|
"provider": config.llm.provider or "none",
|
|
117
171
|
"model": config.llm.model or "",
|
|
118
172
|
"has_key": bool(config.llm.api_key),
|
|
119
|
-
"endpoint":
|
|
173
|
+
"endpoint": _endpoint_host,
|
|
120
174
|
"capabilities": {
|
|
121
175
|
"llm_available": bool(config.llm.provider),
|
|
122
176
|
"cross_encoder": config.retrieval.use_cross_encoder if hasattr(config, 'retrieval') else False,
|
|
123
177
|
},
|
|
124
178
|
}
|
|
125
179
|
except Exception as e:
|
|
126
|
-
return
|
|
180
|
+
return _internal_error()
|
|
127
181
|
|
|
128
182
|
|
|
129
183
|
@router.put("/mode")
|
|
130
184
|
async def set_mode(request: Request):
|
|
131
185
|
"""Switch operating mode. Body: {"mode": "a"|"b"|"c"}"""
|
|
186
|
+
# SEC-H-01: mode switch reconfigures the whole engine — admin-only (MANAGE)
|
|
187
|
+
# on top of machine auth. Owner keeps MANAGE; personal mode unaffected.
|
|
188
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
189
|
+
require_manage(request)
|
|
132
190
|
try:
|
|
133
191
|
body = await request.json()
|
|
134
192
|
new_mode = body.get("mode", "").lower()
|
|
@@ -195,7 +253,7 @@ async def set_mode(request: Request):
|
|
|
195
253
|
"message": "Embedding re-indexing will run on next recall." if needs_reindex else "",
|
|
196
254
|
}
|
|
197
255
|
except Exception as e:
|
|
198
|
-
return
|
|
256
|
+
return _internal_error()
|
|
199
257
|
|
|
200
258
|
|
|
201
259
|
@router.post("/mode/set")
|
|
@@ -204,6 +262,9 @@ async def set_full_config(request: Request):
|
|
|
204
262
|
|
|
205
263
|
V3.4.24: Also accepts embedding_* fields for custom embedding endpoints.
|
|
206
264
|
"""
|
|
265
|
+
# SEC-H-01: sets the LLM provider + API key for the whole engine — admin-only.
|
|
266
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
267
|
+
require_manage(request)
|
|
207
268
|
try:
|
|
208
269
|
body = await request.json()
|
|
209
270
|
new_mode = body.get("mode", "a").lower()
|
|
@@ -287,7 +348,7 @@ async def set_full_config(request: Request):
|
|
|
287
348
|
"embedding_dimension": config.embedding.dimension,
|
|
288
349
|
}
|
|
289
350
|
except Exception as e:
|
|
290
|
-
return
|
|
351
|
+
return _internal_error()
|
|
291
352
|
|
|
292
353
|
|
|
293
354
|
# ── V3.4.24: Embedding Configuration ────────────────────────────────
|
|
@@ -309,12 +370,15 @@ async def get_embedding_config(request: Request):
|
|
|
309
370
|
"mode": config.mode.value,
|
|
310
371
|
}
|
|
311
372
|
except Exception as e:
|
|
312
|
-
return
|
|
373
|
+
return _internal_error()
|
|
313
374
|
|
|
314
375
|
|
|
315
376
|
@router.put("/embedding/config")
|
|
316
377
|
async def set_embedding_config(request: Request):
|
|
317
378
|
"""Update embedding configuration independently of mode switch."""
|
|
379
|
+
# SEC-H-01: swapping the embedding provider/key affects all recall — admin-only.
|
|
380
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
381
|
+
require_manage(request)
|
|
318
382
|
try:
|
|
319
383
|
body = await request.json()
|
|
320
384
|
from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
|
|
@@ -362,7 +426,7 @@ async def set_embedding_config(request: Request):
|
|
|
362
426
|
"needs_reindex": needs_reindex,
|
|
363
427
|
}
|
|
364
428
|
except Exception as e:
|
|
365
|
-
return
|
|
429
|
+
return _internal_error()
|
|
366
430
|
|
|
367
431
|
|
|
368
432
|
@router.get("/scope/config")
|
|
@@ -374,12 +438,15 @@ async def get_scope_config(request: Request):
|
|
|
374
438
|
config = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
375
439
|
return {"success": True, **config.scope.as_dict()}
|
|
376
440
|
except Exception as exc:
|
|
377
|
-
return
|
|
441
|
+
return _internal_error()
|
|
378
442
|
|
|
379
443
|
|
|
380
444
|
@router.put("/scope/config")
|
|
381
445
|
async def set_scope_config(request: Request):
|
|
382
446
|
"""Validate, persist, and hot-apply explicit multi-scope defaults."""
|
|
447
|
+
# SEC-H-01: scope defaults govern cross-profile recall visibility — admin-only.
|
|
448
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
449
|
+
require_manage(request)
|
|
383
450
|
try:
|
|
384
451
|
body = await request.json()
|
|
385
452
|
from superlocalmemory.core.config import SLMConfig, ScopeConfig
|
|
@@ -411,15 +478,15 @@ async def set_scope_config(request: Request):
|
|
|
411
478
|
await _apply_runtime_config(request, config, mode_change=False)
|
|
412
479
|
return {"success": True, **config.scope.as_dict()}
|
|
413
480
|
except Exception as exc:
|
|
414
|
-
return
|
|
481
|
+
return _internal_error()
|
|
415
482
|
|
|
416
483
|
|
|
417
484
|
@router.post("/embedding/test")
|
|
418
485
|
async def test_embedding_endpoint(request: Request):
|
|
419
486
|
"""Test connectivity to a custom embedding endpoint."""
|
|
487
|
+
_require_manage(request)
|
|
420
488
|
try:
|
|
421
489
|
import httpx
|
|
422
|
-
from urllib.parse import urlparse
|
|
423
490
|
body = await request.json()
|
|
424
491
|
endpoint = body.get("api_endpoint", "").rstrip("/")
|
|
425
492
|
model = body.get("model_name", "test")
|
|
@@ -428,12 +495,10 @@ async def test_embedding_endpoint(request: Request):
|
|
|
428
495
|
if not endpoint:
|
|
429
496
|
return JSONResponse({"error": "No endpoint provided"}, status_code=400)
|
|
430
497
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if host in ("169.254.169.254", "metadata.google.internal"):
|
|
436
|
-
return JSONResponse({"error": "Cloud metadata endpoints not allowed"}, status_code=400)
|
|
498
|
+
client_host = request.client.host if request.client else ""
|
|
499
|
+
error = _validate_provider_url(endpoint, client_host)
|
|
500
|
+
if error:
|
|
501
|
+
return JSONResponse({"error": error}, status_code=400)
|
|
437
502
|
|
|
438
503
|
if not endpoint.endswith("/embeddings"):
|
|
439
504
|
endpoint = f"{endpoint}/embeddings"
|
|
@@ -475,8 +540,9 @@ async def embed_ping():
|
|
|
475
540
|
from .helpers import get_engine_lazy
|
|
476
541
|
# We just need to confirm the route is alive — engine check is optional
|
|
477
542
|
return {"ok": True}
|
|
478
|
-
except Exception
|
|
479
|
-
|
|
543
|
+
except Exception:
|
|
544
|
+
logger.exception("embedder liveness probe failed")
|
|
545
|
+
return JSONResponse({"ok": False, "error": "Internal server error"}, status_code=503)
|
|
480
546
|
|
|
481
547
|
|
|
482
548
|
@router.post("/embed")
|
|
@@ -509,7 +575,7 @@ async def embed_texts(request: Request):
|
|
|
509
575
|
)
|
|
510
576
|
return {"embeddings": embeddings}
|
|
511
577
|
except Exception as e:
|
|
512
|
-
return
|
|
578
|
+
return _internal_error()
|
|
513
579
|
|
|
514
580
|
|
|
515
581
|
def _validate_provider_url(url: str, client_host: str) -> str | None:
|
|
@@ -636,7 +702,8 @@ async def test_provider(request: Request):
|
|
|
636
702
|
except httpx.HTTPStatusError as e:
|
|
637
703
|
return {"success": False, "error": f"HTTP {e.response.status_code}: Invalid key or endpoint"}
|
|
638
704
|
except Exception as e:
|
|
639
|
-
|
|
705
|
+
logger.exception("verify_key failed")
|
|
706
|
+
return {"success": False, "error": "internal error"}
|
|
640
707
|
|
|
641
708
|
|
|
642
709
|
@router.get("/ollama/status")
|
|
@@ -666,7 +733,8 @@ async def list_providers():
|
|
|
666
733
|
from superlocalmemory.core.config import SLMConfig
|
|
667
734
|
return {"providers": SLMConfig.provider_presets()}
|
|
668
735
|
except Exception as exc:
|
|
669
|
-
|
|
736
|
+
logger.exception("list_providers failed")
|
|
737
|
+
return {"error": "could not load provider list", "providers": []}
|
|
670
738
|
|
|
671
739
|
|
|
672
740
|
@router.get("/provider")
|
|
@@ -685,12 +753,20 @@ async def get_provider():
|
|
|
685
753
|
"has_key": bool(key),
|
|
686
754
|
}
|
|
687
755
|
except Exception as exc:
|
|
688
|
-
|
|
756
|
+
logger.exception("get_provider failed")
|
|
757
|
+
return {"error": "could not load configuration", "provider": "unknown"}
|
|
689
758
|
|
|
690
759
|
|
|
691
760
|
@router.put("/provider")
|
|
692
761
|
async def set_provider(request: Request):
|
|
693
762
|
"""Set LLM provider. Body: {"provider": "openai", "api_key": "...", "model": "..."}"""
|
|
763
|
+
# Swapping the provider + API key redirects all LLM traffic (prompt/output
|
|
764
|
+
# interception risk) — admin-only, on top of mutation auth.
|
|
765
|
+
from superlocalmemory.server.route_mutations import authorize_route_mutation
|
|
766
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
767
|
+
_auth = authorize_route_mutation(request, operation="update",
|
|
768
|
+
source_agent_id="http-set-provider")
|
|
769
|
+
require_manage(request)
|
|
694
770
|
try:
|
|
695
771
|
body = await request.json()
|
|
696
772
|
provider = body.get("provider", "")
|
|
@@ -720,7 +796,7 @@ async def set_provider(request: Request):
|
|
|
720
796
|
|
|
721
797
|
return {"success": True, "provider": provider, "model": model}
|
|
722
798
|
except Exception as e:
|
|
723
|
-
return
|
|
799
|
+
return _internal_error()
|
|
724
800
|
|
|
725
801
|
|
|
726
802
|
# ── Recall Trace ─────────────────────────────────────────────
|
|
@@ -739,6 +815,7 @@ async def recall_trace(request: Request):
|
|
|
739
815
|
body = await request.json()
|
|
740
816
|
query = body.get("query", "")
|
|
741
817
|
limit = body.get("limit", 10)
|
|
818
|
+
window = body.get("window", "") or ""
|
|
742
819
|
|
|
743
820
|
# Use daemon engine — already loaded, shares warm page cache.
|
|
744
821
|
# run_in_executor keeps event loop alive so browser doesn't abort.
|
|
@@ -751,7 +828,7 @@ async def recall_trace(request: Request):
|
|
|
751
828
|
t0 = _time.monotonic()
|
|
752
829
|
response = await loop.run_in_executor(
|
|
753
830
|
None,
|
|
754
|
-
lambda: engine.recall(query, limit=limit, fast=False),
|
|
831
|
+
lambda: engine.recall(query, limit=limit, fast=False, window=window or None),
|
|
755
832
|
)
|
|
756
833
|
elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
|
|
757
834
|
|
|
@@ -784,7 +861,7 @@ async def recall_trace(request: Request):
|
|
|
784
861
|
**recall_response_metadata(response),
|
|
785
862
|
}
|
|
786
863
|
except Exception as e:
|
|
787
|
-
return
|
|
864
|
+
return _internal_error()
|
|
788
865
|
|
|
789
866
|
|
|
790
867
|
def _record_learning_signals(query: str, results: list) -> None:
|
|
@@ -874,7 +951,7 @@ async def trust_dashboard(request: Request):
|
|
|
874
951
|
"profile": pid,
|
|
875
952
|
}
|
|
876
953
|
except Exception as e:
|
|
877
|
-
return
|
|
954
|
+
return _internal_error()
|
|
878
955
|
|
|
879
956
|
|
|
880
957
|
# ── Math Health ──────────────────────────────────────────────
|
|
@@ -916,7 +993,7 @@ async def math_health(request: Request):
|
|
|
916
993
|
"note": "config-derived; not a live runtime probe",
|
|
917
994
|
}
|
|
918
995
|
except Exception as e:
|
|
919
|
-
return
|
|
996
|
+
return _internal_error()
|
|
920
997
|
|
|
921
998
|
|
|
922
999
|
# ── Auto-Capture / Auto-Recall Config ────────────────────────
|
|
@@ -930,12 +1007,14 @@ async def get_auto_capture_config():
|
|
|
930
1007
|
rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
|
|
931
1008
|
return {"config": rules.get_capture_config()}
|
|
932
1009
|
except Exception as exc:
|
|
933
|
-
|
|
1010
|
+
logger.exception("get_auto_capture_config failed")
|
|
1011
|
+
return {"error": "internal error", "config": {}}
|
|
934
1012
|
|
|
935
1013
|
|
|
936
1014
|
@router.put("/auto-capture/config")
|
|
937
1015
|
async def set_auto_capture_config(request: Request):
|
|
938
1016
|
"""Update auto-capture config. Body: {"enabled": true, "capture_decisions": true, ...}"""
|
|
1017
|
+
_require_manage(request)
|
|
939
1018
|
try:
|
|
940
1019
|
body = await request.json()
|
|
941
1020
|
from superlocalmemory.hooks.rules_engine import RulesEngine
|
|
@@ -947,7 +1026,7 @@ async def set_auto_capture_config(request: Request):
|
|
|
947
1026
|
rules.save(config_path)
|
|
948
1027
|
return {"success": True, "config": rules.get_capture_config()}
|
|
949
1028
|
except Exception as e:
|
|
950
|
-
return
|
|
1029
|
+
return _internal_error()
|
|
951
1030
|
|
|
952
1031
|
|
|
953
1032
|
@router.get("/auto-recall/config")
|
|
@@ -959,12 +1038,14 @@ async def get_auto_recall_config():
|
|
|
959
1038
|
rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
|
|
960
1039
|
return {"config": rules.get_recall_config()}
|
|
961
1040
|
except Exception as exc:
|
|
962
|
-
|
|
1041
|
+
logger.exception("get_auto_recall_config failed")
|
|
1042
|
+
return {"error": "internal error", "config": {}}
|
|
963
1043
|
|
|
964
1044
|
|
|
965
1045
|
@router.put("/auto-recall/config")
|
|
966
1046
|
async def set_auto_recall_config(request: Request):
|
|
967
1047
|
"""Update auto-recall config."""
|
|
1048
|
+
_require_manage(request)
|
|
968
1049
|
try:
|
|
969
1050
|
body = await request.json()
|
|
970
1051
|
from superlocalmemory.hooks.rules_engine import RulesEngine
|
|
@@ -976,7 +1057,7 @@ async def set_auto_recall_config(request: Request):
|
|
|
976
1057
|
rules.save(config_path)
|
|
977
1058
|
return {"success": True, "config": rules.get_recall_config()}
|
|
978
1059
|
except Exception as e:
|
|
979
|
-
return
|
|
1060
|
+
return _internal_error()
|
|
980
1061
|
|
|
981
1062
|
|
|
982
1063
|
# ── IDE Status ───────────────────────────────────────────────
|
|
@@ -989,7 +1070,8 @@ async def ide_status():
|
|
|
989
1070
|
connector = IDEConnector()
|
|
990
1071
|
return {"ides": connector.get_status()}
|
|
991
1072
|
except Exception as exc:
|
|
992
|
-
|
|
1073
|
+
logger.exception("ide_status failed")
|
|
1074
|
+
return {"error": "internal error", "ides": []}
|
|
993
1075
|
|
|
994
1076
|
|
|
995
1077
|
@router.post("/ide/connect")
|
|
@@ -1009,7 +1091,7 @@ async def ide_connect(request: Request):
|
|
|
1009
1091
|
results = connector.connect_all()
|
|
1010
1092
|
return {"results": results}
|
|
1011
1093
|
except Exception as e:
|
|
1012
|
-
return
|
|
1094
|
+
return _internal_error()
|
|
1013
1095
|
|
|
1014
1096
|
|
|
1015
1097
|
# ── Active Memory (V3.1) ────────────────────────────────────
|
|
@@ -1027,7 +1109,8 @@ async def learning_signals():
|
|
|
1027
1109
|
pid = config.active_profile
|
|
1028
1110
|
return {"success": True, **signals.get_signal_stats(pid)}
|
|
1029
1111
|
except Exception as exc:
|
|
1030
|
-
|
|
1112
|
+
logger.exception("learning_signals failed")
|
|
1113
|
+
return {"success": False, "error": "internal error"}
|
|
1031
1114
|
|
|
1032
1115
|
|
|
1033
1116
|
@router.post("/learning/consolidate")
|
|
@@ -1049,6 +1132,7 @@ async def run_consolidation(request: Request):
|
|
|
1049
1132
|
|
|
1050
1133
|
config = SLMConfig.load()
|
|
1051
1134
|
learning_db = DB_PATH.parent / "learning.db"
|
|
1135
|
+
_require_manage_for_profile(request, config.active_profile)
|
|
1052
1136
|
authorization = authorize_route_mutation(
|
|
1053
1137
|
request,
|
|
1054
1138
|
operation="update",
|
|
@@ -1078,7 +1162,8 @@ async def run_consolidation(request: Request):
|
|
|
1078
1162
|
except HTTPException:
|
|
1079
1163
|
raise
|
|
1080
1164
|
except Exception as exc:
|
|
1081
|
-
|
|
1165
|
+
logger.exception("run_consolidation failed")
|
|
1166
|
+
return {"success": False, "error": "internal error"}
|
|
1082
1167
|
|
|
1083
1168
|
|
|
1084
1169
|
@router.get("/hooks/status")
|
|
@@ -1088,7 +1173,8 @@ async def hooks_status():
|
|
|
1088
1173
|
from superlocalmemory.hooks.claude_code_hooks import check_status
|
|
1089
1174
|
return {"success": True, **check_status()}
|
|
1090
1175
|
except Exception as exc:
|
|
1091
|
-
|
|
1176
|
+
logger.exception("hooks_status failed")
|
|
1177
|
+
return {"success": False, "error": "internal error"}
|
|
1092
1178
|
|
|
1093
1179
|
|
|
1094
1180
|
# ── Phase 6: V3.2 API Endpoints ──────────────────────────────
|
|
@@ -1153,7 +1239,7 @@ async def get_auto_invoke_config(request: Request):
|
|
|
1153
1239
|
"last_invocation": persisted.get("last_invocation", None),
|
|
1154
1240
|
}
|
|
1155
1241
|
except Exception as e:
|
|
1156
|
-
return
|
|
1242
|
+
return _internal_error()
|
|
1157
1243
|
|
|
1158
1244
|
|
|
1159
1245
|
# ── 2. PUT /api/v3/auto-invoke/config ─────────────────────────
|
|
@@ -1164,6 +1250,7 @@ async def set_auto_invoke_config(request: Request):
|
|
|
1164
1250
|
|
|
1165
1251
|
Body: {"enabled": true, "min_score": 0.15, "weights": {...}}
|
|
1166
1252
|
"""
|
|
1253
|
+
_require_manage(request)
|
|
1167
1254
|
try:
|
|
1168
1255
|
body = await request.json()
|
|
1169
1256
|
|
|
@@ -1192,7 +1279,7 @@ async def set_auto_invoke_config(request: Request):
|
|
|
1192
1279
|
|
|
1193
1280
|
return updated
|
|
1194
1281
|
except Exception as e:
|
|
1195
|
-
return
|
|
1282
|
+
return _internal_error()
|
|
1196
1283
|
|
|
1197
1284
|
|
|
1198
1285
|
# ── 3. GET /api/v3/associations ───────────────────────────────
|
|
@@ -1205,10 +1292,10 @@ async def get_associations(
|
|
|
1205
1292
|
profile: str = "",
|
|
1206
1293
|
):
|
|
1207
1294
|
"""Get association edges for a profile with content previews."""
|
|
1295
|
+
pid = _resolve_profile(request, profile)
|
|
1208
1296
|
try:
|
|
1209
1297
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1210
1298
|
import sqlite3
|
|
1211
|
-
pid = profile or get_active_profile()
|
|
1212
1299
|
|
|
1213
1300
|
if not DB_PATH.exists():
|
|
1214
1301
|
return {"edges": [], "total": 0}
|
|
@@ -1264,7 +1351,7 @@ async def get_associations(
|
|
|
1264
1351
|
|
|
1265
1352
|
return {"edges": edges, "total": total}
|
|
1266
1353
|
except Exception as e:
|
|
1267
|
-
return
|
|
1354
|
+
return _internal_error()
|
|
1268
1355
|
|
|
1269
1356
|
|
|
1270
1357
|
# ── 4. GET /api/v3/associations/stats ─────────────────────────
|
|
@@ -1272,10 +1359,10 @@ async def get_associations(
|
|
|
1272
1359
|
@router.get("/associations/stats")
|
|
1273
1360
|
async def get_association_stats(request: Request, profile: str = ""):
|
|
1274
1361
|
"""Get association graph statistics."""
|
|
1362
|
+
pid = _resolve_profile(request, profile)
|
|
1275
1363
|
try:
|
|
1276
1364
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1277
1365
|
import sqlite3
|
|
1278
|
-
pid = profile or get_active_profile()
|
|
1279
1366
|
|
|
1280
1367
|
if not DB_PATH.exists():
|
|
1281
1368
|
return {
|
|
@@ -1365,7 +1452,7 @@ async def get_association_stats(request: Request, profile: str = ""):
|
|
|
1365
1452
|
"top_connected_facts": top_facts,
|
|
1366
1453
|
}
|
|
1367
1454
|
except Exception as e:
|
|
1368
|
-
return
|
|
1455
|
+
return _internal_error()
|
|
1369
1456
|
|
|
1370
1457
|
|
|
1371
1458
|
# ── 5. GET /api/v3/consolidation/status ───────────────────────
|
|
@@ -1373,12 +1460,12 @@ async def get_association_stats(request: Request, profile: str = ""):
|
|
|
1373
1460
|
@router.get("/consolidation/status")
|
|
1374
1461
|
async def get_consolidation_status(request: Request, profile: str = ""):
|
|
1375
1462
|
"""Get consolidation status and last run results."""
|
|
1463
|
+
pid = _resolve_profile(request, profile)
|
|
1376
1464
|
try:
|
|
1377
1465
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1378
1466
|
from superlocalmemory.core.config import SLMConfig
|
|
1379
1467
|
import sqlite3
|
|
1380
1468
|
|
|
1381
|
-
pid = profile or get_active_profile()
|
|
1382
1469
|
config = SLMConfig.load()
|
|
1383
1470
|
cons_cfg = config.consolidation
|
|
1384
1471
|
|
|
@@ -1452,7 +1539,7 @@ async def get_consolidation_status(request: Request, profile: str = ""):
|
|
|
1452
1539
|
conn.close()
|
|
1453
1540
|
return result
|
|
1454
1541
|
except Exception as e:
|
|
1455
|
-
return
|
|
1542
|
+
return _internal_error()
|
|
1456
1543
|
|
|
1457
1544
|
|
|
1458
1545
|
# ── 6. POST /api/v3/consolidation/trigger ─────────────────────
|
|
@@ -1471,7 +1558,8 @@ async def trigger_consolidation(request: Request):
|
|
|
1471
1558
|
profile = body.get("profile", "")
|
|
1472
1559
|
|
|
1473
1560
|
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
1474
|
-
pid = profile
|
|
1561
|
+
pid = _resolve_mutation_profile(profile)
|
|
1562
|
+
_require_manage_for_profile(request, pid)
|
|
1475
1563
|
authorization = authorize_route_mutation(
|
|
1476
1564
|
request,
|
|
1477
1565
|
operation="update",
|
|
@@ -1486,42 +1574,54 @@ async def trigger_consolidation(request: Request):
|
|
|
1486
1574
|
# path unconditionally. That dead branch is removed; consolidation
|
|
1487
1575
|
# always runs directly against a lease-protected DB connection so a
|
|
1488
1576
|
# concurrent profile switch cannot commit mid-consolidation.
|
|
1577
|
+
#
|
|
1578
|
+
# v3.4.64: ConsolidationEngine.consolidate() is CPU/IO bound (seconds
|
|
1579
|
+
# to minutes). Calling it directly in an async route blocks the ASGI
|
|
1580
|
+
# event loop. Moved into asyncio.to_thread() so the event loop stays
|
|
1581
|
+
# live. The runtime.operation() lease is acquired INSIDE the thread —
|
|
1582
|
+
# blocking a thread is fine; blocking the event loop is not.
|
|
1583
|
+
import asyncio as _asyncio
|
|
1489
1584
|
from superlocalmemory.core.config import SLMConfig
|
|
1490
1585
|
from superlocalmemory.storage.database import DatabaseManager
|
|
1491
1586
|
from superlocalmemory.storage import schema as _schema
|
|
1492
1587
|
from superlocalmemory.core.consolidation_engine import ConsolidationEngine
|
|
1493
1588
|
from superlocalmemory.server.profile_runtime import get_profile_runtime
|
|
1494
1589
|
|
|
1495
|
-
|
|
1496
|
-
with runtime.operation():
|
|
1497
|
-
config = SLMConfig.load()
|
|
1498
|
-
db = DatabaseManager(config.db_path)
|
|
1499
|
-
db.initialize(_schema)
|
|
1500
|
-
|
|
1501
|
-
engine = ConsolidationEngine(
|
|
1502
|
-
db=db, config=config.consolidation, slm_config=config,
|
|
1503
|
-
)
|
|
1504
|
-
result = engine.consolidate(profile_id=pid, lightweight=lightweight)
|
|
1590
|
+
_app_state = request.app.state
|
|
1505
1591
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1592
|
+
def _run_consolidation() -> dict:
|
|
1593
|
+
runtime = get_profile_runtime(_app_state)
|
|
1594
|
+
with runtime.operation():
|
|
1595
|
+
config = SLMConfig.load()
|
|
1596
|
+
db = DatabaseManager(config.db_path)
|
|
1597
|
+
db.initialize(_schema)
|
|
1598
|
+
engine = ConsolidationEngine(
|
|
1599
|
+
db=db, config=config.consolidation, slm_config=config,
|
|
1510
1600
|
)
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1601
|
+
res = engine.consolidate(profile_id=pid, lightweight=lightweight)
|
|
1602
|
+
# v3.4.1: Auto-trigger behavioral pattern mining after consolidation
|
|
1603
|
+
try:
|
|
1604
|
+
from superlocalmemory.learning.consolidation_worker import (
|
|
1605
|
+
ConsolidationWorker,
|
|
1606
|
+
)
|
|
1607
|
+
learning_db = config.base_dir / "learning.db"
|
|
1608
|
+
cw = ConsolidationWorker(str(config.db_path), str(learning_db))
|
|
1609
|
+
pattern_count = cw._generate_patterns(pid, False)
|
|
1610
|
+
res["patterns_mined"] = pattern_count
|
|
1611
|
+
logger.info(
|
|
1612
|
+
"Auto-mined %d patterns after consolidation", pattern_count
|
|
1613
|
+
)
|
|
1614
|
+
except Exception as exc:
|
|
1615
|
+
logger.debug("Pattern mining after consolidation failed: %s", exc)
|
|
1616
|
+
return res
|
|
1518
1617
|
|
|
1618
|
+
result = await _asyncio.to_thread(_run_consolidation)
|
|
1519
1619
|
authorization.complete()
|
|
1520
1620
|
return {"success": True, **result}
|
|
1521
1621
|
except HTTPException:
|
|
1522
1622
|
raise
|
|
1523
1623
|
except Exception as e:
|
|
1524
|
-
return
|
|
1624
|
+
return _internal_error()
|
|
1525
1625
|
|
|
1526
1626
|
|
|
1527
1627
|
# ── 7. GET /api/v3/core-memory ────────────────────────────────
|
|
@@ -1529,10 +1629,10 @@ async def trigger_consolidation(request: Request):
|
|
|
1529
1629
|
@router.get("/core-memory")
|
|
1530
1630
|
async def get_core_memory(request: Request, profile: str = ""):
|
|
1531
1631
|
"""Get all Core Memory blocks for a profile."""
|
|
1632
|
+
pid = _resolve_profile(request, profile)
|
|
1532
1633
|
try:
|
|
1533
1634
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1534
1635
|
import sqlite3
|
|
1535
|
-
pid = profile or get_active_profile()
|
|
1536
1636
|
|
|
1537
1637
|
if not DB_PATH.exists():
|
|
1538
1638
|
return {"blocks": [], "total_chars": 0, "char_limit": 2000}
|
|
@@ -1567,7 +1667,7 @@ async def get_core_memory(request: Request, profile: str = ""):
|
|
|
1567
1667
|
|
|
1568
1668
|
return {"blocks": blocks, "total_chars": total_chars, "char_limit": 2000}
|
|
1569
1669
|
except Exception as e:
|
|
1570
|
-
return
|
|
1670
|
+
return _internal_error()
|
|
1571
1671
|
|
|
1572
1672
|
|
|
1573
1673
|
# ── 8. PUT /api/v3/core-memory/{block_id} ─────────────────────
|
|
@@ -1650,7 +1750,7 @@ async def update_core_memory_block(block_id: str, request: Request):
|
|
|
1650
1750
|
except HTTPException:
|
|
1651
1751
|
raise
|
|
1652
1752
|
except Exception as e:
|
|
1653
|
-
return
|
|
1753
|
+
return _internal_error()
|
|
1654
1754
|
|
|
1655
1755
|
|
|
1656
1756
|
# ── 9. GET /api/v3/vector-store/status ────────────────────────
|
|
@@ -1658,6 +1758,7 @@ async def update_core_memory_block(block_id: str, request: Request):
|
|
|
1658
1758
|
@router.get("/vector-store/status")
|
|
1659
1759
|
async def get_vector_store_status(request: Request, profile: str = ""):
|
|
1660
1760
|
"""Get VectorStore health and statistics."""
|
|
1761
|
+
pid = _resolve_profile(request, profile)
|
|
1661
1762
|
try:
|
|
1662
1763
|
from superlocalmemory.core.config import SLMConfig
|
|
1663
1764
|
from superlocalmemory.server.routes.helpers import DB_PATH
|
|
@@ -1688,7 +1789,8 @@ async def get_vector_store_status(request: Request, profile: str = ""):
|
|
|
1688
1789
|
try:
|
|
1689
1790
|
conn = sqlite3.connect(str(DB_PATH))
|
|
1690
1791
|
count = conn.execute(
|
|
1691
|
-
"SELECT COUNT(*) FROM embedding_metadata"
|
|
1792
|
+
"SELECT COUNT(*) FROM embedding_metadata WHERE profile_id = ?",
|
|
1793
|
+
(pid,),
|
|
1692
1794
|
).fetchone()[0]
|
|
1693
1795
|
result["total_vectors"] = count
|
|
1694
1796
|
conn.close()
|
|
@@ -1697,7 +1799,7 @@ async def get_vector_store_status(request: Request, profile: str = ""):
|
|
|
1697
1799
|
|
|
1698
1800
|
return result
|
|
1699
1801
|
except Exception as e:
|
|
1700
|
-
return
|
|
1802
|
+
return _internal_error()
|
|
1701
1803
|
|
|
1702
1804
|
|
|
1703
1805
|
# ── Phase 10: V3.3 API Endpoints ────────────────────────────
|
|
@@ -1718,10 +1820,10 @@ async def get_vector_store_status(request: Request, profile: str = ""):
|
|
|
1718
1820
|
@router.get("/forgetting/stats")
|
|
1719
1821
|
async def forgetting_stats(request: Request, profile: str = ""):
|
|
1720
1822
|
"""Get memory retention zone distribution."""
|
|
1823
|
+
pid = _resolve_profile(request, profile)
|
|
1721
1824
|
try:
|
|
1722
1825
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1723
1826
|
import sqlite3 as _sqlite3
|
|
1724
|
-
pid = profile or get_active_profile()
|
|
1725
1827
|
|
|
1726
1828
|
zones = {"active": 0, "warm": 0, "cold": 0, "archive": 0, "forgotten": 0}
|
|
1727
1829
|
total = 0
|
|
@@ -1752,7 +1854,7 @@ async def forgetting_stats(request: Request, profile: str = ""):
|
|
|
1752
1854
|
conn.close()
|
|
1753
1855
|
return {"total": total, "zones": zones}
|
|
1754
1856
|
except Exception as e:
|
|
1755
|
-
return
|
|
1857
|
+
return _internal_error()
|
|
1756
1858
|
|
|
1757
1859
|
|
|
1758
1860
|
# ── 1b. POST /api/v3/forgetting/run ─────────────────────────
|
|
@@ -1769,7 +1871,8 @@ async def run_forgetting(request: Request):
|
|
|
1769
1871
|
|
|
1770
1872
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1771
1873
|
import sqlite3 as _sqlite3
|
|
1772
|
-
pid = profile
|
|
1874
|
+
pid = _resolve_mutation_profile(profile)
|
|
1875
|
+
_require_manage_for_profile(request, pid)
|
|
1773
1876
|
|
|
1774
1877
|
if not DB_PATH.exists():
|
|
1775
1878
|
return {"success": False, "error": "Database not found"}
|
|
@@ -1828,8 +1931,9 @@ async def run_forgetting(request: Request):
|
|
|
1828
1931
|
|
|
1829
1932
|
conn.commit()
|
|
1830
1933
|
except Exception as exc:
|
|
1934
|
+
logger.exception("run_forgetting decay failed")
|
|
1831
1935
|
conn.close()
|
|
1832
|
-
return {"success": False, "error":
|
|
1936
|
+
return {"success": False, "error": "internal error"}
|
|
1833
1937
|
|
|
1834
1938
|
conn.close()
|
|
1835
1939
|
authorization.complete()
|
|
@@ -1837,7 +1941,7 @@ async def run_forgetting(request: Request):
|
|
|
1837
1941
|
except HTTPException:
|
|
1838
1942
|
raise
|
|
1839
1943
|
except Exception as e:
|
|
1840
|
-
return
|
|
1944
|
+
return _internal_error()
|
|
1841
1945
|
|
|
1842
1946
|
|
|
1843
1947
|
# ── 1c. GET /api/v3/quantization/stats ──────────────────────
|
|
@@ -1845,10 +1949,10 @@ async def run_forgetting(request: Request):
|
|
|
1845
1949
|
@router.get("/quantization/stats")
|
|
1846
1950
|
async def quantization_stats(request: Request, profile: str = ""):
|
|
1847
1951
|
"""Get embedding quantization tier distribution."""
|
|
1952
|
+
pid = _resolve_profile(request, profile)
|
|
1848
1953
|
try:
|
|
1849
1954
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1850
1955
|
import sqlite3 as _sqlite3
|
|
1851
|
-
pid = profile or get_active_profile()
|
|
1852
1956
|
|
|
1853
1957
|
tiers = {"float32": 0, "int8": 0, "polar4": 0, "polar2": 0}
|
|
1854
1958
|
total = 0
|
|
@@ -1900,7 +2004,7 @@ async def quantization_stats(request: Request, profile: str = ""):
|
|
|
1900
2004
|
conn.close()
|
|
1901
2005
|
return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
|
|
1902
2006
|
except Exception as e:
|
|
1903
|
-
return
|
|
2007
|
+
return _internal_error()
|
|
1904
2008
|
|
|
1905
2009
|
|
|
1906
2010
|
# ── 1d. GET /api/v3/ccq/blocks ──────────────────────────────
|
|
@@ -1908,10 +2012,10 @@ async def quantization_stats(request: Request, profile: str = ""):
|
|
|
1908
2012
|
@router.get("/ccq/blocks")
|
|
1909
2013
|
async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
|
|
1910
2014
|
"""Get CCQ consolidated blocks."""
|
|
2015
|
+
pid = _resolve_profile(request, profile)
|
|
1911
2016
|
try:
|
|
1912
2017
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1913
2018
|
import sqlite3 as _sqlite3
|
|
1914
|
-
pid = profile or get_active_profile()
|
|
1915
2019
|
|
|
1916
2020
|
if not DB_PATH.exists():
|
|
1917
2021
|
return {"blocks": [], "total": 0}
|
|
@@ -1960,7 +2064,7 @@ async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
|
|
|
1960
2064
|
conn.close()
|
|
1961
2065
|
return {"blocks": blocks, "total": total}
|
|
1962
2066
|
except Exception as e:
|
|
1963
|
-
return
|
|
2067
|
+
return _internal_error()
|
|
1964
2068
|
|
|
1965
2069
|
|
|
1966
2070
|
# ── 1e. GET /api/v3/soft-prompts ─────────────────────────────
|
|
@@ -1968,10 +2072,10 @@ async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
|
|
|
1968
2072
|
@router.get("/soft-prompts")
|
|
1969
2073
|
async def get_soft_prompts(request: Request, profile: str = ""):
|
|
1970
2074
|
"""Get active soft prompt templates."""
|
|
2075
|
+
pid = _resolve_profile(request, profile)
|
|
1971
2076
|
try:
|
|
1972
2077
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
1973
2078
|
import sqlite3 as _sqlite3
|
|
1974
|
-
pid = profile or get_active_profile()
|
|
1975
2079
|
|
|
1976
2080
|
if not DB_PATH.exists():
|
|
1977
2081
|
return {"prompts": [], "total": 0, "total_tokens": 0}
|
|
@@ -2013,7 +2117,7 @@ async def get_soft_prompts(request: Request, profile: str = ""):
|
|
|
2013
2117
|
conn.close()
|
|
2014
2118
|
return {"prompts": prompts, "total": len(prompts), "total_tokens": total_tokens}
|
|
2015
2119
|
except Exception as e:
|
|
2016
|
-
return
|
|
2120
|
+
return _internal_error()
|
|
2017
2121
|
|
|
2018
2122
|
|
|
2019
2123
|
# ── 1f. GET /api/v3/health/processes ─────────────────────────
|
|
@@ -2021,6 +2125,7 @@ async def get_soft_prompts(request: Request, profile: str = ""):
|
|
|
2021
2125
|
@router.get("/health/processes")
|
|
2022
2126
|
async def process_health(request: Request):
|
|
2023
2127
|
"""Get SLM process health status."""
|
|
2128
|
+
_resolve_profile(request)
|
|
2024
2129
|
try:
|
|
2025
2130
|
import os as _os
|
|
2026
2131
|
|
|
@@ -2060,7 +2165,7 @@ async def process_health(request: Request):
|
|
|
2060
2165
|
"healthy": processes["parent"]["status"] != "dead",
|
|
2061
2166
|
}
|
|
2062
2167
|
except Exception as e:
|
|
2063
|
-
return
|
|
2168
|
+
return _internal_error()
|
|
2064
2169
|
|
|
2065
2170
|
|
|
2066
2171
|
# ── 1g. GET /api/v3/v33/overview ─────────────────────────────
|
|
@@ -2075,10 +2180,10 @@ async def get_graph_communities(request: Request, profile: str = ""):
|
|
|
2075
2180
|
at consolidation time). Falls back to inline word frequency if labels
|
|
2076
2181
|
not yet computed.
|
|
2077
2182
|
"""
|
|
2183
|
+
pid = _resolve_profile(request, profile)
|
|
2078
2184
|
try:
|
|
2079
2185
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
2080
2186
|
import sqlite3
|
|
2081
|
-
pid = profile or get_active_profile()
|
|
2082
2187
|
|
|
2083
2188
|
if not DB_PATH.exists():
|
|
2084
2189
|
return {"communities": [], "total": 0}
|
|
@@ -2192,7 +2297,7 @@ async def get_graph_communities(request: Request, profile: str = ""):
|
|
|
2192
2297
|
return {"communities": communities, "total": len(communities)}
|
|
2193
2298
|
|
|
2194
2299
|
except Exception as e:
|
|
2195
|
-
return
|
|
2300
|
+
return _internal_error()
|
|
2196
2301
|
|
|
2197
2302
|
|
|
2198
2303
|
@router.post("/graph/run-communities")
|
|
@@ -2205,6 +2310,7 @@ async def run_community_detection(request: Request):
|
|
|
2205
2310
|
from superlocalmemory.core.graph_analyzer import GraphAnalyzer
|
|
2206
2311
|
|
|
2207
2312
|
pid = get_active_profile()
|
|
2313
|
+
_require_manage_for_profile(request, pid)
|
|
2208
2314
|
authorization = authorize_route_mutation(
|
|
2209
2315
|
request,
|
|
2210
2316
|
operation="update",
|
|
@@ -2221,16 +2327,16 @@ async def run_community_detection(request: Request):
|
|
|
2221
2327
|
except HTTPException:
|
|
2222
2328
|
raise
|
|
2223
2329
|
except Exception as e:
|
|
2224
|
-
return
|
|
2330
|
+
return _internal_error()
|
|
2225
2331
|
|
|
2226
2332
|
|
|
2227
2333
|
@router.get("/v33/overview")
|
|
2228
2334
|
async def v33_overview(request: Request, profile: str = ""):
|
|
2229
2335
|
"""Get SLM 3.3 feature overview -- all new capabilities at a glance."""
|
|
2336
|
+
pid = _resolve_profile(request, profile)
|
|
2230
2337
|
try:
|
|
2231
2338
|
from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
|
|
2232
2339
|
import sqlite3 as _sqlite3
|
|
2233
|
-
pid = profile or get_active_profile()
|
|
2234
2340
|
|
|
2235
2341
|
overview: dict = {
|
|
2236
2342
|
"version": "3.3",
|
|
@@ -2354,4 +2460,56 @@ async def v33_overview(request: Request, profile: str = ""):
|
|
|
2354
2460
|
conn.close()
|
|
2355
2461
|
return overview
|
|
2356
2462
|
except Exception as e:
|
|
2357
|
-
return
|
|
2463
|
+
return _internal_error()
|
|
2464
|
+
|
|
2465
|
+
|
|
2466
|
+
# ── MCP Profiles ─────────────────────────────────────────────
|
|
2467
|
+
|
|
2468
|
+
@router.get("/mcp/profiles")
|
|
2469
|
+
async def get_mcp_profiles(request: Request):
|
|
2470
|
+
"""MCP profile summary: current profile, all profiles with tool counts and names.
|
|
2471
|
+
|
|
2472
|
+
Reads only the env var SLM_MCP_PROFILE and the pure-data profiles module.
|
|
2473
|
+
No engine required — safe to call at any point in the daemon lifecycle.
|
|
2474
|
+
|
|
2475
|
+
Note on 'current': when SLM_MCP_PROFILE is unset the MCP server falls
|
|
2476
|
+
back to the legacy _ESSENTIAL_TOOLS set. The UI reports 'core' for that
|
|
2477
|
+
state because 'core' is the recommended named-profile equivalent for new
|
|
2478
|
+
installs and is the closest documented starting point for users.
|
|
2479
|
+
"""
|
|
2480
|
+
try:
|
|
2481
|
+
from superlocalmemory.mcp.profiles import (
|
|
2482
|
+
_PROFILE_DEFINITIONS,
|
|
2483
|
+
_PROFILE_ALIASES,
|
|
2484
|
+
PROFILE_DESCRIPTIONS,
|
|
2485
|
+
)
|
|
2486
|
+
|
|
2487
|
+
raw_profile = os.environ.get("SLM_MCP_PROFILE", "").strip().lower()
|
|
2488
|
+
canonical = _PROFILE_ALIASES.get(raw_profile, raw_profile)
|
|
2489
|
+
# Blank env var, "whole", or an unknown value all resolve to "core"
|
|
2490
|
+
# for UI display purposes (safe, conservative default).
|
|
2491
|
+
if not canonical or canonical == "whole" or canonical not in _PROFILE_DEFINITIONS:
|
|
2492
|
+
current = "core"
|
|
2493
|
+
else:
|
|
2494
|
+
current = canonical
|
|
2495
|
+
|
|
2496
|
+
profiles_out: dict = {}
|
|
2497
|
+
for name, tool_set in _PROFILE_DEFINITIONS.items():
|
|
2498
|
+
profiles_out[name] = {
|
|
2499
|
+
"count": len(tool_set),
|
|
2500
|
+
"tools": sorted(tool_set),
|
|
2501
|
+
"description": PROFILE_DESCRIPTIONS.get(name, ""),
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
all_tools: set = set()
|
|
2505
|
+
for tool_set in _PROFILE_DEFINITIONS.values():
|
|
2506
|
+
all_tools.update(tool_set)
|
|
2507
|
+
|
|
2508
|
+
return {
|
|
2509
|
+
"current": current,
|
|
2510
|
+
"profiles": profiles_out,
|
|
2511
|
+
"aliases": dict(_PROFILE_ALIASES),
|
|
2512
|
+
"total_tools": len(all_tools),
|
|
2513
|
+
}
|
|
2514
|
+
except Exception:
|
|
2515
|
+
return _internal_error()
|