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
|
@@ -12,13 +12,17 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import re
|
|
15
|
-
from
|
|
15
|
+
from datetime import datetime, timedelta, timezone
|
|
16
16
|
|
|
17
17
|
from fastapi import APIRouter, HTTPException, Request
|
|
18
18
|
from pydantic import BaseModel
|
|
19
19
|
|
|
20
20
|
router = APIRouter(prefix="/mesh", tags=["mesh"])
|
|
21
21
|
|
|
22
|
+
_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost"})
|
|
23
|
+
_STALE_AFTER = timedelta(minutes=5)
|
|
24
|
+
_EXPIRE_AFTER = timedelta(minutes=30)
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
# -- Request models --
|
|
24
28
|
|
|
@@ -88,6 +92,7 @@ def _get_broker(request: Request):
|
|
|
88
92
|
client_host = request.client.host if request.client else ""
|
|
89
93
|
if client_host not in ("127.0.0.1", "::1", "localhost"):
|
|
90
94
|
import hmac
|
|
95
|
+
|
|
91
96
|
from superlocalmemory.core.security_primitives import verify_install_token
|
|
92
97
|
|
|
93
98
|
# Path 1: install token — dashboard/browser callers hold this and
|
|
@@ -120,6 +125,19 @@ def _get_broker(request: Request):
|
|
|
120
125
|
return broker
|
|
121
126
|
|
|
122
127
|
|
|
128
|
+
def _active_profile() -> str:
|
|
129
|
+
"""Resolve the tenant (profile) for this mesh request.
|
|
130
|
+
|
|
131
|
+
The mesh is a per-tenant coordination bus: peers, messages, shared state,
|
|
132
|
+
and locks are all scoped to the active profile so one tenant never sees
|
|
133
|
+
another's coordination traffic. Resolved from the request ContextVar set by
|
|
134
|
+
ProfileRuntimeMiddleware (falls back to the configured active profile).
|
|
135
|
+
"""
|
|
136
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
137
|
+
|
|
138
|
+
return get_active_profile()
|
|
139
|
+
|
|
140
|
+
|
|
123
141
|
_SECRET_KEY = re.compile(
|
|
124
142
|
r"(?:^|[_\-.])(api[_\-.]?key|secret|token|password|credential)(?:$|[_\-.])",
|
|
125
143
|
re.IGNORECASE,
|
|
@@ -140,56 +158,195 @@ def _reject_secret_state(key: str, value: str) -> None:
|
|
|
140
158
|
# -- Routes --
|
|
141
159
|
|
|
142
160
|
@router.post("/register")
|
|
143
|
-
|
|
161
|
+
def register(req: RegisterRequest, request: Request):
|
|
144
162
|
broker = _get_broker(request)
|
|
145
163
|
if not req.session_id:
|
|
146
164
|
raise HTTPException(400, detail="session_id required")
|
|
147
165
|
return broker.register_peer(
|
|
148
166
|
req.session_id, req.summary, req.host, req.port,
|
|
149
|
-
req.project_path, req.agent_type,
|
|
167
|
+
req.project_path, req.agent_type, profile_id=_active_profile(),
|
|
150
168
|
)
|
|
151
169
|
|
|
152
170
|
|
|
153
171
|
@router.post("/deregister")
|
|
154
|
-
|
|
172
|
+
def deregister(req: DeregisterRequest, request: Request):
|
|
155
173
|
broker = _get_broker(request)
|
|
156
|
-
result = broker.deregister_peer(req.peer_id)
|
|
174
|
+
result = broker.deregister_peer(req.peer_id, profile_id=_active_profile())
|
|
157
175
|
if not result.get("ok"):
|
|
158
176
|
raise HTTPException(404, detail=result.get("error", "peer not found"))
|
|
159
177
|
return result
|
|
160
178
|
|
|
161
179
|
|
|
180
|
+
def _peer_activity_counts(request: Request, session_ids: list[str]) -> dict:
|
|
181
|
+
"""Per-session {tool_count, memory_count} for the active profile.
|
|
182
|
+
|
|
183
|
+
Each mesh peer IS an agent session, so its activity is the tool events it
|
|
184
|
+
logged and the memories it contributed under the current profile. Batched
|
|
185
|
+
to a single grouped query per table. Fail-soft: returns {} on any error so
|
|
186
|
+
the peer list still renders.
|
|
187
|
+
"""
|
|
188
|
+
if not session_ids:
|
|
189
|
+
return {}
|
|
190
|
+
try:
|
|
191
|
+
from superlocalmemory.server.routes.helpers import (
|
|
192
|
+
get_active_profile,
|
|
193
|
+
get_engine_lazy,
|
|
194
|
+
)
|
|
195
|
+
engine = get_engine_lazy(request.app.state)
|
|
196
|
+
if engine is None:
|
|
197
|
+
return {}
|
|
198
|
+
profile = get_active_profile()
|
|
199
|
+
placeholders = ",".join("?" * len(session_ids))
|
|
200
|
+
params = [profile, *session_ids]
|
|
201
|
+
counts: dict[str, dict] = {sid: {"tool_count": 0, "memory_count": 0}
|
|
202
|
+
for sid in session_ids}
|
|
203
|
+
for table, key in (("tool_events", "tool_count"),
|
|
204
|
+
("memories", "memory_count")):
|
|
205
|
+
try:
|
|
206
|
+
rows = engine._db.execute(
|
|
207
|
+
f"SELECT session_id, COUNT(*) AS n FROM {table} "
|
|
208
|
+
f"WHERE profile_id = ? AND session_id IN ({placeholders}) "
|
|
209
|
+
"GROUP BY session_id",
|
|
210
|
+
tuple(params),
|
|
211
|
+
)
|
|
212
|
+
for r in rows:
|
|
213
|
+
d = dict(r)
|
|
214
|
+
sid = d.get("session_id")
|
|
215
|
+
if sid in counts:
|
|
216
|
+
counts[sid][key] = d.get("n", 0)
|
|
217
|
+
except Exception:
|
|
218
|
+
continue
|
|
219
|
+
return counts
|
|
220
|
+
except Exception:
|
|
221
|
+
return {}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _parse_heartbeat(value: object) -> datetime | None:
|
|
225
|
+
"""Parse a stored heartbeat as UTC without trusting malformed values."""
|
|
226
|
+
if not isinstance(value, str) or not value:
|
|
227
|
+
return None
|
|
228
|
+
try:
|
|
229
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
230
|
+
except ValueError:
|
|
231
|
+
return None
|
|
232
|
+
if parsed.tzinfo is None:
|
|
233
|
+
return parsed.replace(tzinfo=timezone.utc)
|
|
234
|
+
return parsed.astimezone(timezone.utc)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _mesh_read_model(records: list[dict]) -> tuple[list[dict], list[dict]]:
|
|
238
|
+
"""Return bounded remote peers and local sessions with read-time liveness.
|
|
239
|
+
|
|
240
|
+
The broker persists local agent sessions in ``mesh_peers`` for messaging.
|
|
241
|
+
That storage detail must not make them look like remote mesh neighbours in
|
|
242
|
+
the dashboard. Classification is read-only so an ordinary dashboard GET
|
|
243
|
+
neither mutates a live database nor waits for the five-minute cleanup loop.
|
|
244
|
+
"""
|
|
245
|
+
now = datetime.now(timezone.utc)
|
|
246
|
+
remote: list[dict] = []
|
|
247
|
+
local: list[dict] = []
|
|
248
|
+
for record in records:
|
|
249
|
+
heartbeat = _parse_heartbeat(record.get("last_heartbeat"))
|
|
250
|
+
if heartbeat is None:
|
|
251
|
+
continue
|
|
252
|
+
age = now - heartbeat
|
|
253
|
+
if age >= _EXPIRE_AFTER:
|
|
254
|
+
continue
|
|
255
|
+
stale_at = heartbeat + _STALE_AFTER
|
|
256
|
+
expires_at = heartbeat + _EXPIRE_AFTER
|
|
257
|
+
status = "active" if age < _STALE_AFTER else "stale"
|
|
258
|
+
normalized = {
|
|
259
|
+
**record,
|
|
260
|
+
"status": status,
|
|
261
|
+
"stale_at": stale_at.isoformat(),
|
|
262
|
+
"expires_at": expires_at.isoformat(),
|
|
263
|
+
}
|
|
264
|
+
if str(record.get("host") or "").lower() in _LOOPBACK_HOSTS:
|
|
265
|
+
local.append(normalized)
|
|
266
|
+
else:
|
|
267
|
+
remote.append(normalized)
|
|
268
|
+
return remote, local
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _mesh_counts(remote: list[dict], local: list[dict]) -> dict:
|
|
272
|
+
"""Expose active counts separately while retaining the legacy total key."""
|
|
273
|
+
active_remote = sum(peer["status"] == "active" for peer in remote)
|
|
274
|
+
active_local = sum(session["status"] == "active" for session in local)
|
|
275
|
+
return {
|
|
276
|
+
"peer_count": active_remote + active_local,
|
|
277
|
+
"active_peer_count": active_remote + active_local,
|
|
278
|
+
"remote_peer_count": active_remote,
|
|
279
|
+
"local_session_count": active_local,
|
|
280
|
+
"stale_peer_count": sum(peer["status"] == "stale" for peer in remote),
|
|
281
|
+
"stale_local_session_count": sum(session["status"] == "stale" for session in local),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
162
285
|
@router.get("/peers")
|
|
163
|
-
|
|
286
|
+
def peers(request: Request, view: str = "all"):
|
|
164
287
|
broker = _get_broker(request)
|
|
165
|
-
|
|
288
|
+
if view not in {"all", "remote", "local"}:
|
|
289
|
+
raise HTTPException(422, detail="view must be all, remote, or local")
|
|
290
|
+
remote_peers, local_sessions = _mesh_read_model(
|
|
291
|
+
broker.list_all_peers(_active_profile()),
|
|
292
|
+
)
|
|
293
|
+
peer_list = (
|
|
294
|
+
remote_peers if view == "remote" else
|
|
295
|
+
local_sessions if view == "local" else
|
|
296
|
+
[*remote_peers, *local_sessions]
|
|
297
|
+
)
|
|
298
|
+
session_ids = [p.get("session_id") for p in peer_list if p.get("session_id")]
|
|
299
|
+
counts = _peer_activity_counts(request, session_ids)
|
|
300
|
+
enriched = []
|
|
301
|
+
for p in peer_list:
|
|
302
|
+
c = counts.get(p.get("session_id"), {})
|
|
303
|
+
enriched.append({
|
|
304
|
+
**p,
|
|
305
|
+
"tool_count": c.get("tool_count", 0),
|
|
306
|
+
"memory_count": c.get("memory_count", 0),
|
|
307
|
+
})
|
|
308
|
+
return {
|
|
309
|
+
"peers": enriched,
|
|
310
|
+
"remote_peers": remote_peers,
|
|
311
|
+
"local_sessions": local_sessions,
|
|
312
|
+
"view": view,
|
|
313
|
+
**_mesh_counts(remote_peers, local_sessions),
|
|
314
|
+
}
|
|
166
315
|
|
|
167
316
|
|
|
168
317
|
@router.post("/heartbeat")
|
|
169
|
-
|
|
318
|
+
def heartbeat(req: HeartbeatRequest, request: Request):
|
|
319
|
+
"""Update peer liveness without blocking the daemon's async event loop."""
|
|
170
320
|
broker = _get_broker(request)
|
|
171
|
-
result = broker.heartbeat(req.peer_id)
|
|
321
|
+
result = broker.heartbeat(req.peer_id, profile_id=_active_profile())
|
|
172
322
|
if not result.get("ok"):
|
|
173
323
|
raise HTTPException(404, detail=result.get("error", "peer not found"))
|
|
174
324
|
return result
|
|
175
325
|
|
|
176
326
|
|
|
177
327
|
@router.post("/summary")
|
|
178
|
-
|
|
328
|
+
def summary(req: SummaryRequest, request: Request):
|
|
179
329
|
broker = _get_broker(request)
|
|
180
|
-
result = broker.update_summary(req.peer_id, req.summary
|
|
330
|
+
result = broker.update_summary(req.peer_id, req.summary,
|
|
331
|
+
profile_id=_active_profile())
|
|
181
332
|
if not result.get("ok"):
|
|
182
333
|
raise HTTPException(404, detail=result.get("error", "peer not found"))
|
|
183
334
|
return result
|
|
184
335
|
|
|
185
336
|
|
|
186
337
|
@router.post("/send")
|
|
187
|
-
|
|
338
|
+
def send(req: SendRequest, request: Request):
|
|
188
339
|
broker = _get_broker(request)
|
|
189
340
|
to_target = req.to_peer or req.to # v3.4.6: accept both field names
|
|
190
341
|
if not to_target:
|
|
191
342
|
raise HTTPException(400, detail="'to' or 'to_peer' required")
|
|
192
|
-
|
|
343
|
+
profile = _active_profile()
|
|
344
|
+
# This sync FastAPI route already runs in the worker thread pool, so the
|
|
345
|
+
# broker's SQLite retries and optional remote HTTP delivery cannot block
|
|
346
|
+
# the daemon event loop.
|
|
347
|
+
result = broker.send_message(
|
|
348
|
+
req.from_peer, to_target, req.content, req.type, "", profile,
|
|
349
|
+
)
|
|
193
350
|
if not result.get("ok"):
|
|
194
351
|
status = 413 if "too large" in result.get("error", "") else 404
|
|
195
352
|
raise HTTPException(status, detail=result.get("error", ""))
|
|
@@ -197,66 +354,78 @@ async def send(req: SendRequest, request: Request):
|
|
|
197
354
|
|
|
198
355
|
|
|
199
356
|
@router.get("/inbox/{peer_id}")
|
|
200
|
-
|
|
357
|
+
def inbox(peer_id: str, request: Request, project_path: str = ""):
|
|
201
358
|
broker = _get_broker(request)
|
|
202
|
-
return {"messages": broker.get_inbox(peer_id, project_path
|
|
359
|
+
return {"messages": broker.get_inbox(peer_id, project_path,
|
|
360
|
+
profile_id=_active_profile())}
|
|
203
361
|
|
|
204
362
|
|
|
205
363
|
@router.post("/inbox/{peer_id}/read")
|
|
206
|
-
|
|
364
|
+
def mark_read(peer_id: str, req: ReadRequest, request: Request):
|
|
207
365
|
broker = _get_broker(request)
|
|
208
|
-
return broker.mark_read(peer_id, req.message_ids
|
|
366
|
+
return broker.mark_read(peer_id, req.message_ids,
|
|
367
|
+
profile_id=_active_profile())
|
|
209
368
|
|
|
210
369
|
|
|
211
370
|
@router.get("/pending/{peer_id}")
|
|
212
|
-
|
|
371
|
+
def pending(peer_id: str, request: Request, project_path: str = ""):
|
|
213
372
|
"""Get pending broadcast/project messages for this peer."""
|
|
214
373
|
broker = _get_broker(request)
|
|
215
|
-
messages = broker.get_pending(peer_id, project_path
|
|
374
|
+
messages = broker.get_pending(peer_id, project_path,
|
|
375
|
+
profile_id=_active_profile())
|
|
216
376
|
return {"messages": messages, "count": len(messages)}
|
|
217
377
|
|
|
218
378
|
|
|
219
379
|
@router.get("/state")
|
|
220
|
-
|
|
380
|
+
def state_all(request: Request):
|
|
221
381
|
broker = _get_broker(request)
|
|
222
|
-
return {"state": broker.get_state()}
|
|
382
|
+
return {"state": broker.get_state(profile_id=_active_profile())}
|
|
223
383
|
|
|
224
384
|
|
|
225
385
|
@router.post("/state")
|
|
226
|
-
|
|
386
|
+
def state_set(req: StateSetRequest, request: Request):
|
|
227
387
|
broker = _get_broker(request)
|
|
228
388
|
if not req.key:
|
|
229
389
|
raise HTTPException(400, detail="key required")
|
|
230
390
|
_reject_secret_state(req.key, req.value)
|
|
231
|
-
return broker.set_state(req.key, req.value, req.set_by
|
|
391
|
+
return broker.set_state(req.key, req.value, req.set_by,
|
|
392
|
+
profile_id=_active_profile())
|
|
232
393
|
|
|
233
394
|
|
|
234
395
|
@router.get("/state/{key}")
|
|
235
|
-
|
|
396
|
+
def state_get(key: str, request: Request):
|
|
236
397
|
broker = _get_broker(request)
|
|
237
|
-
result = broker.get_state_key(key)
|
|
398
|
+
result = broker.get_state_key(key, profile_id=_active_profile())
|
|
238
399
|
if result is None:
|
|
239
400
|
raise HTTPException(404, detail="key not found")
|
|
240
401
|
return result
|
|
241
402
|
|
|
242
403
|
|
|
243
404
|
@router.post("/lock")
|
|
244
|
-
|
|
405
|
+
def lock(req: LockRequest, request: Request):
|
|
245
406
|
broker = _get_broker(request)
|
|
246
407
|
if not req.file_path or not req.locked_by:
|
|
247
408
|
raise HTTPException(400, detail="file_path and locked_by required")
|
|
248
409
|
if req.action not in ("acquire", "release", "query"):
|
|
249
410
|
raise HTTPException(400, detail="action must be acquire, release, or query")
|
|
250
|
-
return broker.lock_action(req.file_path, req.locked_by, req.action
|
|
411
|
+
return broker.lock_action(req.file_path, req.locked_by, req.action,
|
|
412
|
+
profile_id=_active_profile())
|
|
251
413
|
|
|
252
414
|
|
|
253
415
|
@router.get("/events")
|
|
254
|
-
|
|
416
|
+
def events(request: Request):
|
|
255
417
|
broker = _get_broker(request)
|
|
256
|
-
return {"events": broker.get_events()}
|
|
418
|
+
return {"events": broker.get_events(profile_id=_active_profile())}
|
|
257
419
|
|
|
258
420
|
|
|
259
421
|
@router.get("/status")
|
|
260
|
-
|
|
422
|
+
def status(request: Request):
|
|
261
423
|
broker = _get_broker(request)
|
|
262
|
-
|
|
424
|
+
broker_status = broker.get_status(profile_id=_active_profile())
|
|
425
|
+
remote_peers, local_sessions = _mesh_read_model(
|
|
426
|
+
broker.list_all_peers(_active_profile()),
|
|
427
|
+
)
|
|
428
|
+
return {
|
|
429
|
+
**broker_status,
|
|
430
|
+
**_mesh_counts(remote_peers, local_sessions),
|
|
431
|
+
}
|
|
@@ -71,6 +71,36 @@ async def put_config(body: ConfigUpdateRequest) -> dict[str, Any]:
|
|
|
71
71
|
raise HTTPException(status_code=500, detail=str(exc))
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
@router.get("/status")
|
|
75
|
+
async def get_status() -> dict[str, Any]:
|
|
76
|
+
"""Concise enabled/disabled summary of each optimize sub-feature (M3).
|
|
77
|
+
|
|
78
|
+
REST-convention companion to `slm optimize status` (which reads config
|
|
79
|
+
locally). Returns a per-feature on/off view, the compression mode, and a
|
|
80
|
+
top-level `healthy` flag so external pollers have a stable endpoint.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
from superlocalmemory.optimize.config import get_shared_store
|
|
84
|
+
cfg = get_shared_store().get()
|
|
85
|
+
return {
|
|
86
|
+
"healthy": True,
|
|
87
|
+
"enabled": cfg.enabled,
|
|
88
|
+
"features": {
|
|
89
|
+
"proxy": cfg.proxy_enabled,
|
|
90
|
+
"cache": cfg.cache_enabled,
|
|
91
|
+
"compression": cfg.compress_enabled,
|
|
92
|
+
"semantic_cache": cfg.semantic_enabled,
|
|
93
|
+
},
|
|
94
|
+
"compress_mode": cfg.compress_mode,
|
|
95
|
+
"config_version": cfg.config_version,
|
|
96
|
+
}
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.warning("GET /api/optimize/status failed: %s", exc)
|
|
99
|
+
# Fail-open: report unhealthy rather than 500 so a poller can
|
|
100
|
+
# distinguish "daemon up but optimize degraded" from "daemon down".
|
|
101
|
+
return {"healthy": False, "error": "internal error"}
|
|
102
|
+
|
|
103
|
+
|
|
74
104
|
@router.get("/savings")
|
|
75
105
|
async def get_savings() -> dict[str, Any]:
|
|
76
106
|
"""Return savings estimates from live metrics + CacheDB.
|
|
@@ -91,7 +121,9 @@ async def get_savings() -> dict[str, Any]:
|
|
|
91
121
|
)
|
|
92
122
|
|
|
93
123
|
# F-03 fix: use module-level singleton instead of per-request instantiation
|
|
94
|
-
|
|
124
|
+
# DASH-V6 fix: attribute may EXIST but be None; getattr default only
|
|
125
|
+
# covers the missing case, so coerce None -> default to avoid .lower() crash.
|
|
126
|
+
active_model = getattr(cfg, "active_model", None) or "anthropic"
|
|
95
127
|
# Determine provider from model
|
|
96
128
|
provider = "anthropic"
|
|
97
129
|
for key in ("anthropic", "openai", "gemini"):
|
|
@@ -154,6 +154,7 @@ def _upsert_cache(
|
|
|
154
154
|
content: str, fact_ids: list[str],
|
|
155
155
|
) -> None:
|
|
156
156
|
from superlocalmemory.core.context_cache import CacheEntry, ContextCache
|
|
157
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
157
158
|
cache = ContextCache()
|
|
158
159
|
try:
|
|
159
160
|
cache.upsert(CacheEntry(
|
|
@@ -163,6 +164,7 @@ def _upsert_cache(
|
|
|
163
164
|
fact_ids=tuple(fact_ids),
|
|
164
165
|
provenance="prewarm_post_tool",
|
|
165
166
|
computed_at=int(time.time()),
|
|
167
|
+
profile_id=get_active_profile(), # so non-default profiles hit
|
|
166
168
|
))
|
|
167
169
|
finally:
|
|
168
170
|
cache.close()
|
|
@@ -28,11 +28,18 @@ from .helpers import (
|
|
|
28
28
|
from superlocalmemory.server.profile_runtime import (
|
|
29
29
|
commit_daemon_profile_switch,
|
|
30
30
|
get_profile_runtime,
|
|
31
|
+
TransitionDrainTimeout,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
logger = logging.getLogger("superlocalmemory.routes.profiles")
|
|
34
35
|
router = APIRouter()
|
|
35
36
|
|
|
37
|
+
|
|
38
|
+
def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
39
|
+
"""SEC-H-02: log full traceback server-side; return a generic message to the client."""
|
|
40
|
+
logger.exception("profiles route error")
|
|
41
|
+
return HTTPException(status_code=500, detail=detail)
|
|
42
|
+
|
|
36
43
|
# WebSocket manager reference (set by ui_server.py at startup)
|
|
37
44
|
ws_manager = None
|
|
38
45
|
|
|
@@ -85,8 +92,8 @@ async def list_profiles(request: Request):
|
|
|
85
92
|
"total_profiles": len(profiles),
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
except Exception
|
|
89
|
-
raise
|
|
95
|
+
except Exception:
|
|
96
|
+
raise _internal_error("Profile list error")
|
|
90
97
|
|
|
91
98
|
|
|
92
99
|
@router.post("/api/profiles/{name}/switch")
|
|
@@ -112,17 +119,36 @@ async def switch_profile(name: str, request: Request):
|
|
|
112
119
|
source_agent_id="http-profile-switch",
|
|
113
120
|
profile_id=name,
|
|
114
121
|
)
|
|
122
|
+
# RBAC (C4): a logged-in user may only activate a workspace they belong
|
|
123
|
+
# to — this is the read boundary. Switching sets the active tenant for
|
|
124
|
+
# subsequent reads, so a non-member must not be able to enter it. The
|
|
125
|
+
# machine owner (no user session) switches freely.
|
|
126
|
+
from superlocalmemory.server.rbac_enforce import resolve_principal
|
|
127
|
+
_principal = resolve_principal(request)
|
|
128
|
+
if _principal.get("kind") == "user":
|
|
129
|
+
_rbac = getattr(request.app.state, "rbac", None)
|
|
130
|
+
if _rbac is not None and _rbac.get_role(_principal["user_id"], name) is None:
|
|
131
|
+
raise HTTPException(
|
|
132
|
+
status_code=403,
|
|
133
|
+
detail="You are not a member of this workspace.",
|
|
134
|
+
)
|
|
115
135
|
runtime = get_profile_runtime(request.app.state)
|
|
116
136
|
previous = runtime.snapshot.profile_id
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
try:
|
|
138
|
+
snapshot = await asyncio.to_thread(
|
|
139
|
+
runtime.transition,
|
|
140
|
+
name,
|
|
141
|
+
lambda prior, target: commit_daemon_profile_switch(
|
|
142
|
+
request.app.state,
|
|
143
|
+
prior,
|
|
144
|
+
target,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
except TransitionDrainTimeout as exc:
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=503,
|
|
150
|
+
detail=str(exc),
|
|
151
|
+
)
|
|
126
152
|
|
|
127
153
|
count = _get_memory_count(name)
|
|
128
154
|
|
|
@@ -143,8 +169,8 @@ async def switch_profile(name: str, request: Request):
|
|
|
143
169
|
|
|
144
170
|
except HTTPException:
|
|
145
171
|
raise
|
|
146
|
-
except Exception
|
|
147
|
-
raise
|
|
172
|
+
except Exception:
|
|
173
|
+
raise _internal_error("Profile switch error")
|
|
148
174
|
|
|
149
175
|
|
|
150
176
|
@router.post("/api/profiles/create")
|
|
@@ -166,18 +192,34 @@ async def create_profile(body: ProfileSwitch, request: Request):
|
|
|
166
192
|
operation="update",
|
|
167
193
|
source_agent_id="http-profile-create",
|
|
168
194
|
)
|
|
195
|
+
# RBAC (C3): creating a tenant is an administrative action.
|
|
196
|
+
from superlocalmemory.access.rbac import Permission as _Perm
|
|
197
|
+
from superlocalmemory.server.rbac_enforce import require_manage as _rbac_manage
|
|
198
|
+
_principal = _rbac_manage(request)
|
|
169
199
|
# Write to BOTH stores atomically
|
|
170
200
|
desc = f'Memory profile: {name}'
|
|
171
201
|
ensure_profile_in_db(name, desc)
|
|
172
202
|
ensure_profile_in_json(name, desc)
|
|
173
203
|
|
|
204
|
+
# A logged-in user who creates a workspace becomes its admin so they
|
|
205
|
+
# can manage it immediately (profile_id == name here). The machine
|
|
206
|
+
# owner needs no membership row (implicit root).
|
|
207
|
+
if _principal.get("kind") == "user":
|
|
208
|
+
rbac = getattr(request.app.state, "rbac", None)
|
|
209
|
+
if rbac is not None:
|
|
210
|
+
try:
|
|
211
|
+
rbac.set_membership(name, _principal["user_id"], "admin",
|
|
212
|
+
added_by=_principal["username"])
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
174
216
|
authorization.complete()
|
|
175
217
|
return {"success": True, "profile": name, "message": f"Profile '{name}' created"}
|
|
176
218
|
|
|
177
219
|
except HTTPException:
|
|
178
220
|
raise
|
|
179
|
-
except Exception
|
|
180
|
-
raise
|
|
221
|
+
except Exception:
|
|
222
|
+
raise _internal_error("Profile create error")
|
|
181
223
|
|
|
182
224
|
|
|
183
225
|
@router.delete("/api/profiles/{name}")
|
|
@@ -204,6 +246,10 @@ async def delete_profile(name: str, request: Request):
|
|
|
204
246
|
source_agent_id="http-profile-delete",
|
|
205
247
|
profile_id=name,
|
|
206
248
|
)
|
|
249
|
+
# RBAC (C3): deleting a tenant is administrative. Check MANAGE on the
|
|
250
|
+
# profile being deleted (not the active one).
|
|
251
|
+
from superlocalmemory.server.rbac_enforce import require_manage as _rbac_manage
|
|
252
|
+
_rbac_manage(request, profile=name)
|
|
207
253
|
# Move data to default before deleting (bypasses CASCADE)
|
|
208
254
|
conn = get_db_connection()
|
|
209
255
|
cursor = conn.cursor()
|
|
@@ -243,5 +289,5 @@ async def delete_profile(name: str, request: Request):
|
|
|
243
289
|
|
|
244
290
|
except HTTPException:
|
|
245
291
|
raise
|
|
246
|
-
except Exception
|
|
247
|
-
raise
|
|
292
|
+
except Exception:
|
|
293
|
+
raise _internal_error("Profile delete error")
|