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
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Dashboard config endpoints — storage, daemon, mesh, trust, forgetting.
|
|
6
|
+
|
|
7
|
+
Each section provides GET (read current) and PUT (validate + persist) routes.
|
|
8
|
+
Writes use the same direct-JSON approach as the evolution config endpoint:
|
|
9
|
+
1. Read config.json as a raw dict (fail-open → defaults when absent/corrupt).
|
|
10
|
+
2. Update only the targeted keys; all other keys — including 'mode' — survive.
|
|
11
|
+
3. Atomic write via a temp file + os.replace (no torn writes).
|
|
12
|
+
|
|
13
|
+
Auth: follows the same pattern as the auto-capture/auto-recall/auto-invoke
|
|
14
|
+
config endpoints — no route-level auth guard. Write-identity is enforced by
|
|
15
|
+
the middleware layer wired in unified_daemon.py / ui.py for non-localhost
|
|
16
|
+
callers. Tests use a bare FastAPI app (no middleware) and monkeypatch
|
|
17
|
+
MEMORY_DIR to a tmp_path.
|
|
18
|
+
|
|
19
|
+
Restart-required semantics:
|
|
20
|
+
- graph_backend / vector_backend: changes take effect only on daemon restart.
|
|
21
|
+
- daemon_port / daemon_legacy_port: changes take effect only on daemon restart.
|
|
22
|
+
- mesh, trust, and forgetting are persisted safely but require restart
|
|
23
|
+
because no complete supported worker-rebind transaction exists for them.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Annotated, Optional
|
|
31
|
+
|
|
32
|
+
from fastapi import APIRouter, Request
|
|
33
|
+
from fastapi.responses import JSONResponse
|
|
34
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool
|
|
35
|
+
|
|
36
|
+
from superlocalmemory.server.config_file import read_config, update_config
|
|
37
|
+
from superlocalmemory.server.routes.helpers import MEMORY_DIR
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
router = APIRouter(prefix="/api/v3", tags=["config"])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _require_admin(request: Request) -> None:
|
|
45
|
+
"""SEC-H-01: system-configuration mutations are admin-only (MANAGE).
|
|
46
|
+
|
|
47
|
+
Machine auth (loopback / credential) is enforced by the daemon middleware;
|
|
48
|
+
this adds the RBAC layer so a logged-in non-admin (viewer/member) in company
|
|
49
|
+
mode cannot change the daemon port, swap the LLM key, or alter the forgetting
|
|
50
|
+
curve. The machine owner keeps MANAGE (personal mode is unaffected). Call
|
|
51
|
+
this BEFORE the handler's try/except so the 401/403 is not swallowed into a
|
|
52
|
+
500.
|
|
53
|
+
"""
|
|
54
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
55
|
+
require_manage(request)
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Allowed backend values
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
_GRAPH_BACKENDS = frozenset({"auto", "sqlite", "cozo"})
|
|
62
|
+
_VECTOR_BACKENDS = frozenset({"auto", "lancedb", "sqlite-vec"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Internal helpers
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _config_path() -> Path:
|
|
71
|
+
"""Return the config.json path, resolved from the active MEMORY_DIR."""
|
|
72
|
+
return MEMORY_DIR / "config.json"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_config() -> dict:
|
|
76
|
+
"""Read one coherent config snapshot."""
|
|
77
|
+
p = _config_path()
|
|
78
|
+
try:
|
|
79
|
+
return read_config(p)
|
|
80
|
+
except (ValueError, OSError) as exc:
|
|
81
|
+
logger.warning("config_api: could not read config.json: %s", exc)
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _update_config(mutator) -> dict:
|
|
86
|
+
"""Run one interprocess-locked read/modify/replace transaction."""
|
|
87
|
+
return update_config(_config_path(), mutator)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Pydantic request models (extra="forbid" → 422 on unknown keys)
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class StorageConfigUpdate(BaseModel):
|
|
96
|
+
model_config = ConfigDict(extra="forbid")
|
|
97
|
+
|
|
98
|
+
graph_backend: Optional[Annotated[str, Field(pattern=r"^(auto|sqlite|cozo)$")]] = None
|
|
99
|
+
vector_backend: Optional[Annotated[str, Field(pattern=r"^(auto|lancedb|sqlite-vec)$")]] = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DaemonConfigUpdate(BaseModel):
|
|
103
|
+
model_config = ConfigDict(extra="forbid")
|
|
104
|
+
|
|
105
|
+
idle_timeout: Optional[int] = Field(None, ge=0)
|
|
106
|
+
port: Optional[int] = Field(None, ge=1, le=65535)
|
|
107
|
+
legacy_port: Optional[int] = Field(None, ge=1, le=65535)
|
|
108
|
+
enable_legacy_port: Optional[StrictBool] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class MeshConfigUpdate(BaseModel):
|
|
112
|
+
model_config = ConfigDict(extra="forbid")
|
|
113
|
+
|
|
114
|
+
enabled: StrictBool
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TrustConfigUpdate(BaseModel):
|
|
118
|
+
model_config = ConfigDict(extra="forbid")
|
|
119
|
+
|
|
120
|
+
use_trust_weighting: Optional[StrictBool] = None
|
|
121
|
+
trust_first_party: Optional[StrictBool] = None
|
|
122
|
+
promotion_min_trust: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ForgettingConfigUpdate(BaseModel):
|
|
126
|
+
model_config = ConfigDict(extra="forbid")
|
|
127
|
+
|
|
128
|
+
enabled: Optional[StrictBool] = None
|
|
129
|
+
alpha: Optional[float] = Field(None, gt=0.0)
|
|
130
|
+
beta: Optional[float] = Field(None, gt=0.0)
|
|
131
|
+
gamma: Optional[float] = Field(None, gt=0.0)
|
|
132
|
+
delta: Optional[float] = Field(None, gt=0.0)
|
|
133
|
+
min_strength: Optional[float] = Field(None, gt=0.0)
|
|
134
|
+
max_strength: Optional[float] = Field(None, gt=0.0)
|
|
135
|
+
archive_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
136
|
+
forget_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
137
|
+
learning_rate: Optional[float] = Field(None, gt=0.0)
|
|
138
|
+
forgetting_drift_scale: Optional[float] = Field(None, gt=0.0)
|
|
139
|
+
trust_kappa: Optional[float] = Field(None, gt=0.0)
|
|
140
|
+
scheduler_interval_minutes: Optional[int] = Field(None, ge=1)
|
|
141
|
+
core_memory_immune: Optional[StrictBool] = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Default value constants (mirrors ForgettingConfig dataclass defaults)
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
_FORGETTING_DEFAULTS: dict = {
|
|
149
|
+
"enabled": True,
|
|
150
|
+
"alpha": 2.0,
|
|
151
|
+
"beta": 1.5,
|
|
152
|
+
"gamma": 1.0,
|
|
153
|
+
"delta": 0.5,
|
|
154
|
+
"min_strength": 0.1,
|
|
155
|
+
"max_strength": 100.0,
|
|
156
|
+
"archive_threshold": 0.2,
|
|
157
|
+
"forget_threshold": 0.05,
|
|
158
|
+
"learning_rate": 1.0,
|
|
159
|
+
"forgetting_drift_scale": 0.5,
|
|
160
|
+
"trust_kappa": 2.0,
|
|
161
|
+
"scheduler_interval_minutes": 30,
|
|
162
|
+
"core_memory_immune": True,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# GET /api/v3/storage/config
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.get("/storage/config")
|
|
172
|
+
def get_storage_config():
|
|
173
|
+
"""Return current storage backend configuration.
|
|
174
|
+
|
|
175
|
+
base_dir is read-only — it is derived from the process namespace and
|
|
176
|
+
cannot be changed via this endpoint.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
data = _read_config()
|
|
180
|
+
return {
|
|
181
|
+
"graph_backend": data.get("graph_backend", "auto"),
|
|
182
|
+
"vector_backend": data.get("vector_backend", "auto"),
|
|
183
|
+
"base_dir": data.get("base_dir", str(MEMORY_DIR)),
|
|
184
|
+
}
|
|
185
|
+
except Exception:
|
|
186
|
+
logger.exception("get_storage_config failed")
|
|
187
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# PUT /api/v3/storage/config
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.put("/storage/config")
|
|
196
|
+
def put_storage_config(request: Request, body: StorageConfigUpdate):
|
|
197
|
+
"""Update graph_backend and/or vector_backend.
|
|
198
|
+
|
|
199
|
+
Both fields require a daemon restart to take effect.
|
|
200
|
+
Returns restart_required: true unconditionally.
|
|
201
|
+
"""
|
|
202
|
+
_require_admin(request)
|
|
203
|
+
try:
|
|
204
|
+
def mutate(data: dict) -> None:
|
|
205
|
+
if body.graph_backend is not None:
|
|
206
|
+
data["graph_backend"] = body.graph_backend
|
|
207
|
+
if body.vector_backend is not None:
|
|
208
|
+
data["vector_backend"] = body.vector_backend
|
|
209
|
+
|
|
210
|
+
data = _update_config(mutate)
|
|
211
|
+
return {
|
|
212
|
+
"graph_backend": data.get("graph_backend", "auto"),
|
|
213
|
+
"vector_backend": data.get("vector_backend", "auto"),
|
|
214
|
+
"base_dir": data.get("base_dir", str(MEMORY_DIR)),
|
|
215
|
+
"restart_required": True,
|
|
216
|
+
}
|
|
217
|
+
except Exception:
|
|
218
|
+
logger.exception("put_storage_config failed")
|
|
219
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# GET /api/v3/daemon/config
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@router.get("/daemon/config")
|
|
228
|
+
def get_daemon_config():
|
|
229
|
+
"""Return current daemon configuration."""
|
|
230
|
+
try:
|
|
231
|
+
data = _read_config()
|
|
232
|
+
return {
|
|
233
|
+
"idle_timeout": data.get("daemon_idle_timeout", 0),
|
|
234
|
+
"port": data.get("daemon_port", 8765),
|
|
235
|
+
"legacy_port": data.get("daemon_legacy_port", 8767),
|
|
236
|
+
"enable_legacy_port": data.get("daemon_enable_legacy_port", True),
|
|
237
|
+
}
|
|
238
|
+
except Exception:
|
|
239
|
+
logger.exception("get_daemon_config failed")
|
|
240
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# PUT /api/v3/daemon/config
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@router.put("/daemon/config")
|
|
249
|
+
def put_daemon_config(request: Request, body: DaemonConfigUpdate):
|
|
250
|
+
"""Update daemon configuration.
|
|
251
|
+
|
|
252
|
+
Port / legacy_port changes require a daemon restart.
|
|
253
|
+
restart_required is True if any port field is included in the request.
|
|
254
|
+
"""
|
|
255
|
+
_require_admin(request)
|
|
256
|
+
try:
|
|
257
|
+
port_changed = False
|
|
258
|
+
|
|
259
|
+
def mutate(data: dict) -> None:
|
|
260
|
+
nonlocal port_changed
|
|
261
|
+
if body.idle_timeout is not None:
|
|
262
|
+
data["daemon_idle_timeout"] = body.idle_timeout
|
|
263
|
+
if body.port is not None:
|
|
264
|
+
data["daemon_port"] = body.port
|
|
265
|
+
port_changed = True
|
|
266
|
+
if body.legacy_port is not None:
|
|
267
|
+
data["daemon_legacy_port"] = body.legacy_port
|
|
268
|
+
port_changed = True
|
|
269
|
+
if body.enable_legacy_port is not None:
|
|
270
|
+
data["daemon_enable_legacy_port"] = body.enable_legacy_port
|
|
271
|
+
|
|
272
|
+
data = _update_config(mutate)
|
|
273
|
+
return {
|
|
274
|
+
"idle_timeout": data.get("daemon_idle_timeout", 0),
|
|
275
|
+
"port": data.get("daemon_port", 8765),
|
|
276
|
+
"legacy_port": data.get("daemon_legacy_port", 8767),
|
|
277
|
+
"enable_legacy_port": data.get("daemon_enable_legacy_port", True),
|
|
278
|
+
"restart_required": port_changed,
|
|
279
|
+
}
|
|
280
|
+
except Exception:
|
|
281
|
+
logger.exception("put_daemon_config failed")
|
|
282
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# GET /api/v3/mesh/config
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@router.get("/mesh/config")
|
|
291
|
+
def get_mesh_config():
|
|
292
|
+
"""Return current mesh configuration."""
|
|
293
|
+
try:
|
|
294
|
+
data = _read_config()
|
|
295
|
+
return {"enabled": data.get("mesh_enabled", True)}
|
|
296
|
+
except Exception:
|
|
297
|
+
logger.exception("get_mesh_config failed")
|
|
298
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# PUT /api/v3/mesh/config
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@router.put("/mesh/config")
|
|
307
|
+
def put_mesh_config(request: Request, body: MeshConfigUpdate):
|
|
308
|
+
"""Persist mesh state; restart is required to rebuild the mesh worker."""
|
|
309
|
+
_require_admin(request)
|
|
310
|
+
try:
|
|
311
|
+
_update_config(
|
|
312
|
+
lambda data: data.update({"mesh_enabled": body.enabled}),
|
|
313
|
+
)
|
|
314
|
+
return {"enabled": body.enabled, "restart_required": True}
|
|
315
|
+
except Exception:
|
|
316
|
+
logger.exception("put_mesh_config failed")
|
|
317
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# GET /api/v3/trust/config
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@router.get("/trust/config")
|
|
326
|
+
def get_trust_config():
|
|
327
|
+
"""Return current trust configuration.
|
|
328
|
+
|
|
329
|
+
Fields are spread across three config sections:
|
|
330
|
+
- retrieval.use_trust_weighting (Bayesian trust in retrieval ranking)
|
|
331
|
+
- injection.trust_first_party (framing of injected context)
|
|
332
|
+
- consolidation.promotion_min_trust (min trust required for promotion)
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
data = _read_config()
|
|
336
|
+
retrieval = data.get("retrieval", {})
|
|
337
|
+
injection = data.get("injection", {})
|
|
338
|
+
consolidation = data.get("consolidation", {})
|
|
339
|
+
return {
|
|
340
|
+
"use_trust_weighting": retrieval.get("use_trust_weighting", True),
|
|
341
|
+
"trust_first_party": injection.get("trust_first_party", False),
|
|
342
|
+
"promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
|
|
343
|
+
}
|
|
344
|
+
except Exception:
|
|
345
|
+
logger.exception("get_trust_config failed")
|
|
346
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# PUT /api/v3/trust/config
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@router.put("/trust/config")
|
|
355
|
+
def put_trust_config(request: Request, body: TrustConfigUpdate):
|
|
356
|
+
"""Update trust configuration.
|
|
357
|
+
|
|
358
|
+
Each field is stored in its canonical config.json sub-section.
|
|
359
|
+
Unrelated keys in those sub-sections are preserved.
|
|
360
|
+
"""
|
|
361
|
+
_require_admin(request)
|
|
362
|
+
try:
|
|
363
|
+
def mutate(data: dict) -> None:
|
|
364
|
+
if body.use_trust_weighting is not None:
|
|
365
|
+
retrieval = data.setdefault("retrieval", {})
|
|
366
|
+
retrieval["use_trust_weighting"] = body.use_trust_weighting
|
|
367
|
+
if body.trust_first_party is not None:
|
|
368
|
+
injection = data.setdefault("injection", {})
|
|
369
|
+
injection["trust_first_party"] = body.trust_first_party
|
|
370
|
+
if body.promotion_min_trust is not None:
|
|
371
|
+
consolidation = data.setdefault("consolidation", {})
|
|
372
|
+
consolidation["promotion_min_trust"] = body.promotion_min_trust
|
|
373
|
+
|
|
374
|
+
data = _update_config(mutate)
|
|
375
|
+
retrieval = data.get("retrieval", {})
|
|
376
|
+
injection = data.get("injection", {})
|
|
377
|
+
consolidation = data.get("consolidation", {})
|
|
378
|
+
return {
|
|
379
|
+
"use_trust_weighting": retrieval.get("use_trust_weighting", True),
|
|
380
|
+
"trust_first_party": injection.get("trust_first_party", False),
|
|
381
|
+
"promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
|
|
382
|
+
"restart_required": True,
|
|
383
|
+
}
|
|
384
|
+
except Exception:
|
|
385
|
+
logger.exception("put_trust_config failed")
|
|
386
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# GET /api/v3/forgetting/config
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@router.get("/forgetting/config")
|
|
395
|
+
def get_forgetting_config():
|
|
396
|
+
"""Return all Ebbinghaus forgetting configuration fields."""
|
|
397
|
+
try:
|
|
398
|
+
data = _read_config()
|
|
399
|
+
stored = data.get("forgetting", {})
|
|
400
|
+
# Return defaults for any field not yet in config.json
|
|
401
|
+
result = {**_FORGETTING_DEFAULTS, **stored}
|
|
402
|
+
# Keep only known fields
|
|
403
|
+
return {k: result[k] for k in _FORGETTING_DEFAULTS}
|
|
404
|
+
except Exception:
|
|
405
|
+
logger.exception("get_forgetting_config failed")
|
|
406
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# PUT /api/v3/forgetting/config
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@router.put("/forgetting/config")
|
|
415
|
+
def put_forgetting_config(request: Request, body: ForgettingConfigUpdate):
|
|
416
|
+
"""Update Ebbinghaus forgetting configuration.
|
|
417
|
+
|
|
418
|
+
Only provided fields are changed; all other forgetting fields are
|
|
419
|
+
preserved. Changes take effect after the daemon restarts.
|
|
420
|
+
"""
|
|
421
|
+
_require_admin(request)
|
|
422
|
+
try:
|
|
423
|
+
updates = body.model_dump(exclude_none=True)
|
|
424
|
+
|
|
425
|
+
def mutate(data: dict) -> None:
|
|
426
|
+
stored = data.get("forgetting", {})
|
|
427
|
+
merged = {**_FORGETTING_DEFAULTS, **stored, **updates}
|
|
428
|
+
data["forgetting"] = merged
|
|
429
|
+
|
|
430
|
+
data = _update_config(mutate)
|
|
431
|
+
merged = data["forgetting"]
|
|
432
|
+
return {
|
|
433
|
+
**{k: merged[k] for k in _FORGETTING_DEFAULTS},
|
|
434
|
+
"restart_required": True,
|
|
435
|
+
}
|
|
436
|
+
except Exception:
|
|
437
|
+
logger.exception("put_forgetting_config failed")
|
|
438
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
@@ -27,6 +27,12 @@ from .helpers import (
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger("superlocalmemory.routes.data_io")
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
32
|
+
"""SEC-H-02: log full traceback server-side; return a generic message to the client."""
|
|
33
|
+
logger.exception("data_io route error")
|
|
34
|
+
return HTTPException(status_code=500, detail=detail)
|
|
35
|
+
|
|
30
36
|
# WebSocket manager reference (set by ui_server.py at startup)
|
|
31
37
|
ws_manager = None
|
|
32
38
|
|
|
@@ -35,11 +41,19 @@ router = APIRouter()
|
|
|
35
41
|
|
|
36
42
|
@router.get("/api/export")
|
|
37
43
|
async def export_memories(
|
|
44
|
+
request: Request,
|
|
38
45
|
format: str = Query("json", pattern="^(json|jsonl|csv)$"),
|
|
39
46
|
category: Optional[str] = None,
|
|
40
47
|
project_name: Optional[str] = None,
|
|
41
48
|
):
|
|
42
49
|
"""Export memories as JSON, JSONL, or CSV."""
|
|
50
|
+
# Bulk data export. This GET is not covered by the mutation middleware and
|
|
51
|
+
# is reached both by a plain fetch and a top-level navigation, neither of
|
|
52
|
+
# which carries a credential header — so gate on the loopback-trusted
|
|
53
|
+
# mutation boundary: local owner allowed, remote uncredentialed fails closed.
|
|
54
|
+
from superlocalmemory.server.write_identity import require_http_mutation_actor
|
|
55
|
+
require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
|
|
56
|
+
actor_kind="data-export")
|
|
43
57
|
try:
|
|
44
58
|
conn = get_db_connection()
|
|
45
59
|
conn.row_factory = dict_factory
|
|
@@ -128,22 +142,29 @@ async def export_memories(
|
|
|
128
142
|
},
|
|
129
143
|
)
|
|
130
144
|
|
|
131
|
-
except Exception
|
|
132
|
-
raise
|
|
145
|
+
except Exception:
|
|
146
|
+
raise _internal_error("Export error")
|
|
133
147
|
|
|
134
148
|
|
|
135
149
|
@router.post("/api/import")
|
|
136
150
|
async def import_memories(request: Request, file: UploadFile = File(...)):
|
|
137
151
|
"""Import memories from JSON file using V3 engine."""
|
|
138
152
|
try:
|
|
139
|
-
|
|
153
|
+
# Bound the upload so a huge file cannot OOM the daemon (read one byte
|
|
154
|
+
# past the cap to detect oversize without buffering the whole payload).
|
|
155
|
+
_MAX_IMPORT_BYTES = 50 * 1024 * 1024
|
|
156
|
+
content = await file.read(_MAX_IMPORT_BYTES + 1)
|
|
157
|
+
if len(content) > _MAX_IMPORT_BYTES:
|
|
158
|
+
raise HTTPException(status_code=413,
|
|
159
|
+
detail="Import file exceeds the 50 MB limit")
|
|
140
160
|
if file.filename and file.filename.endswith('.gz'):
|
|
141
161
|
content = gzip.decompress(content)
|
|
142
162
|
|
|
143
163
|
try:
|
|
144
164
|
data = json.loads(content)
|
|
145
|
-
except json.JSONDecodeError
|
|
146
|
-
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
logger.warning("import: invalid JSON payload")
|
|
167
|
+
raise HTTPException(status_code=400, detail="Invalid JSON format")
|
|
147
168
|
|
|
148
169
|
if isinstance(data, dict) and 'memories' in data:
|
|
149
170
|
memories = data['memories']
|
|
@@ -224,7 +245,8 @@ async def import_memories(request: Request, file: UploadFile = File(...)):
|
|
|
224
245
|
if "UNIQUE constraint failed" in str(e):
|
|
225
246
|
skipped += 1
|
|
226
247
|
else:
|
|
227
|
-
|
|
248
|
+
logger.warning("import: memory %d failed: %s", idx, e)
|
|
249
|
+
errors.append(f"Memory {idx}: import failed")
|
|
228
250
|
|
|
229
251
|
return {
|
|
230
252
|
"success": True, "imported_count": imported,
|
|
@@ -234,5 +256,5 @@ async def import_memories(request: Request, file: UploadFile = File(...)):
|
|
|
234
256
|
|
|
235
257
|
except HTTPException:
|
|
236
258
|
raise
|
|
237
|
-
except Exception
|
|
238
|
-
raise
|
|
259
|
+
except Exception:
|
|
260
|
+
raise _internal_error("Import error")
|