superlocalmemory 3.7.8 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +69 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -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 +94 -10
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +8 -1
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +183 -69
- 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 +384 -56
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -0,0 +1,436 @@
|
|
|
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
|
+
- All other fields: hot-applied on next engine reconfigure cycle.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Annotated, Literal, Optional
|
|
32
|
+
|
|
33
|
+
from fastapi import APIRouter, Request
|
|
34
|
+
from fastapi.responses import JSONResponse
|
|
35
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool
|
|
36
|
+
|
|
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 config.json as a raw dict. Returns {} on missing/corrupt file."""
|
|
77
|
+
p = _config_path()
|
|
78
|
+
if not p.exists():
|
|
79
|
+
return {}
|
|
80
|
+
try:
|
|
81
|
+
return json.loads(p.read_text())
|
|
82
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
83
|
+
logger.warning("config_api: could not read config.json: %s", exc)
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _atomic_write(data: dict) -> None:
|
|
88
|
+
"""Atomic config.json write: .tmp → os.replace."""
|
|
89
|
+
p = _config_path()
|
|
90
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
tmp = p.with_suffix(".json.tmp")
|
|
92
|
+
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
93
|
+
os.replace(tmp, p)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Pydantic request models (extra="forbid" → 422 on unknown keys)
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class StorageConfigUpdate(BaseModel):
|
|
102
|
+
model_config = ConfigDict(extra="forbid")
|
|
103
|
+
|
|
104
|
+
graph_backend: Optional[Annotated[str, Field(pattern=r"^(auto|sqlite|cozo)$")]] = None
|
|
105
|
+
vector_backend: Optional[Annotated[str, Field(pattern=r"^(auto|lancedb|sqlite-vec)$")]] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DaemonConfigUpdate(BaseModel):
|
|
109
|
+
model_config = ConfigDict(extra="forbid")
|
|
110
|
+
|
|
111
|
+
idle_timeout: Optional[int] = Field(None, ge=0)
|
|
112
|
+
port: Optional[int] = Field(None, ge=1, le=65535)
|
|
113
|
+
legacy_port: Optional[int] = Field(None, ge=1, le=65535)
|
|
114
|
+
enable_legacy_port: Optional[StrictBool] = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class MeshConfigUpdate(BaseModel):
|
|
118
|
+
model_config = ConfigDict(extra="forbid")
|
|
119
|
+
|
|
120
|
+
enabled: StrictBool
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TrustConfigUpdate(BaseModel):
|
|
124
|
+
model_config = ConfigDict(extra="forbid")
|
|
125
|
+
|
|
126
|
+
use_trust_weighting: Optional[StrictBool] = None
|
|
127
|
+
trust_first_party: Optional[StrictBool] = None
|
|
128
|
+
promotion_min_trust: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ForgettingConfigUpdate(BaseModel):
|
|
132
|
+
model_config = ConfigDict(extra="forbid")
|
|
133
|
+
|
|
134
|
+
enabled: Optional[StrictBool] = None
|
|
135
|
+
alpha: Optional[float] = Field(None, gt=0.0)
|
|
136
|
+
beta: Optional[float] = Field(None, gt=0.0)
|
|
137
|
+
gamma: Optional[float] = Field(None, gt=0.0)
|
|
138
|
+
delta: Optional[float] = Field(None, gt=0.0)
|
|
139
|
+
min_strength: Optional[float] = Field(None, gt=0.0)
|
|
140
|
+
max_strength: Optional[float] = Field(None, gt=0.0)
|
|
141
|
+
archive_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
142
|
+
forget_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
|
|
143
|
+
learning_rate: Optional[float] = Field(None, gt=0.0)
|
|
144
|
+
forgetting_drift_scale: Optional[float] = Field(None, gt=0.0)
|
|
145
|
+
trust_kappa: Optional[float] = Field(None, gt=0.0)
|
|
146
|
+
scheduler_interval_minutes: Optional[int] = Field(None, ge=1)
|
|
147
|
+
core_memory_immune: Optional[StrictBool] = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Default value constants (mirrors ForgettingConfig dataclass defaults)
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
_FORGETTING_DEFAULTS: dict = {
|
|
155
|
+
"enabled": True,
|
|
156
|
+
"alpha": 2.0,
|
|
157
|
+
"beta": 1.5,
|
|
158
|
+
"gamma": 1.0,
|
|
159
|
+
"delta": 0.5,
|
|
160
|
+
"min_strength": 0.1,
|
|
161
|
+
"max_strength": 100.0,
|
|
162
|
+
"archive_threshold": 0.2,
|
|
163
|
+
"forget_threshold": 0.05,
|
|
164
|
+
"learning_rate": 1.0,
|
|
165
|
+
"forgetting_drift_scale": 0.5,
|
|
166
|
+
"trust_kappa": 2.0,
|
|
167
|
+
"scheduler_interval_minutes": 30,
|
|
168
|
+
"core_memory_immune": True,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# GET /api/v3/storage/config
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@router.get("/storage/config")
|
|
178
|
+
async def get_storage_config():
|
|
179
|
+
"""Return current storage backend configuration.
|
|
180
|
+
|
|
181
|
+
base_dir is read-only — it is derived from the process namespace and
|
|
182
|
+
cannot be changed via this endpoint.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
data = _read_config()
|
|
186
|
+
return {
|
|
187
|
+
"graph_backend": data.get("graph_backend", "auto"),
|
|
188
|
+
"vector_backend": data.get("vector_backend", "auto"),
|
|
189
|
+
"base_dir": data.get("base_dir", str(MEMORY_DIR)),
|
|
190
|
+
}
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
logger.exception("get_storage_config failed")
|
|
193
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# PUT /api/v3/storage/config
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@router.put("/storage/config")
|
|
202
|
+
async def put_storage_config(request: Request, body: StorageConfigUpdate):
|
|
203
|
+
"""Update graph_backend and/or vector_backend.
|
|
204
|
+
|
|
205
|
+
Both fields require a daemon restart to take effect.
|
|
206
|
+
Returns restart_required: true unconditionally.
|
|
207
|
+
"""
|
|
208
|
+
_require_admin(request)
|
|
209
|
+
try:
|
|
210
|
+
data = _read_config()
|
|
211
|
+
if body.graph_backend is not None:
|
|
212
|
+
data["graph_backend"] = body.graph_backend
|
|
213
|
+
if body.vector_backend is not None:
|
|
214
|
+
data["vector_backend"] = body.vector_backend
|
|
215
|
+
_atomic_write(data)
|
|
216
|
+
return {
|
|
217
|
+
"graph_backend": data.get("graph_backend", "auto"),
|
|
218
|
+
"vector_backend": data.get("vector_backend", "auto"),
|
|
219
|
+
"base_dir": data.get("base_dir", str(MEMORY_DIR)),
|
|
220
|
+
"restart_required": True,
|
|
221
|
+
}
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
logger.exception("put_storage_config failed")
|
|
224
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# GET /api/v3/daemon/config
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@router.get("/daemon/config")
|
|
233
|
+
async def get_daemon_config():
|
|
234
|
+
"""Return current daemon configuration."""
|
|
235
|
+
try:
|
|
236
|
+
data = _read_config()
|
|
237
|
+
return {
|
|
238
|
+
"idle_timeout": data.get("daemon_idle_timeout", 0),
|
|
239
|
+
"port": data.get("daemon_port", 8765),
|
|
240
|
+
"legacy_port": data.get("daemon_legacy_port", 8767),
|
|
241
|
+
"enable_legacy_port": data.get("daemon_enable_legacy_port", True),
|
|
242
|
+
}
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.exception("get_daemon_config failed")
|
|
245
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# PUT /api/v3/daemon/config
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@router.put("/daemon/config")
|
|
254
|
+
async def put_daemon_config(request: Request, body: DaemonConfigUpdate):
|
|
255
|
+
"""Update daemon configuration.
|
|
256
|
+
|
|
257
|
+
Port / legacy_port changes require a daemon restart.
|
|
258
|
+
restart_required is True if any port field is included in the request.
|
|
259
|
+
"""
|
|
260
|
+
_require_admin(request)
|
|
261
|
+
try:
|
|
262
|
+
data = _read_config()
|
|
263
|
+
port_changed = False
|
|
264
|
+
if body.idle_timeout is not None:
|
|
265
|
+
data["daemon_idle_timeout"] = body.idle_timeout
|
|
266
|
+
if body.port is not None:
|
|
267
|
+
data["daemon_port"] = body.port
|
|
268
|
+
port_changed = True
|
|
269
|
+
if body.legacy_port is not None:
|
|
270
|
+
data["daemon_legacy_port"] = body.legacy_port
|
|
271
|
+
port_changed = True
|
|
272
|
+
if body.enable_legacy_port is not None:
|
|
273
|
+
data["daemon_enable_legacy_port"] = body.enable_legacy_port
|
|
274
|
+
_atomic_write(data)
|
|
275
|
+
return {
|
|
276
|
+
"idle_timeout": data.get("daemon_idle_timeout", 0),
|
|
277
|
+
"port": data.get("daemon_port", 8765),
|
|
278
|
+
"legacy_port": data.get("daemon_legacy_port", 8767),
|
|
279
|
+
"enable_legacy_port": data.get("daemon_enable_legacy_port", True),
|
|
280
|
+
"restart_required": port_changed,
|
|
281
|
+
}
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
logger.exception("put_daemon_config failed")
|
|
284
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# GET /api/v3/mesh/config
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@router.get("/mesh/config")
|
|
293
|
+
async def get_mesh_config():
|
|
294
|
+
"""Return current mesh configuration."""
|
|
295
|
+
try:
|
|
296
|
+
data = _read_config()
|
|
297
|
+
return {"enabled": data.get("mesh_enabled", True)}
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
logger.exception("get_mesh_config failed")
|
|
300
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
# PUT /api/v3/mesh/config
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@router.put("/mesh/config")
|
|
309
|
+
async def put_mesh_config(request: Request, body: MeshConfigUpdate):
|
|
310
|
+
"""Enable or disable the mesh sync layer."""
|
|
311
|
+
_require_admin(request)
|
|
312
|
+
try:
|
|
313
|
+
data = _read_config()
|
|
314
|
+
data["mesh_enabled"] = body.enabled
|
|
315
|
+
_atomic_write(data)
|
|
316
|
+
return {"enabled": body.enabled}
|
|
317
|
+
except Exception as exc:
|
|
318
|
+
logger.exception("put_mesh_config failed")
|
|
319
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# GET /api/v3/trust/config
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@router.get("/trust/config")
|
|
328
|
+
async def get_trust_config():
|
|
329
|
+
"""Return current trust configuration.
|
|
330
|
+
|
|
331
|
+
Fields are spread across three config sections:
|
|
332
|
+
- retrieval.use_trust_weighting (Bayesian trust in retrieval ranking)
|
|
333
|
+
- injection.trust_first_party (framing of injected context)
|
|
334
|
+
- consolidation.promotion_min_trust (min trust required for promotion)
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
data = _read_config()
|
|
338
|
+
retrieval = data.get("retrieval", {})
|
|
339
|
+
injection = data.get("injection", {})
|
|
340
|
+
consolidation = data.get("consolidation", {})
|
|
341
|
+
return {
|
|
342
|
+
"use_trust_weighting": retrieval.get("use_trust_weighting", True),
|
|
343
|
+
"trust_first_party": injection.get("trust_first_party", False),
|
|
344
|
+
"promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
|
|
345
|
+
}
|
|
346
|
+
except Exception as exc:
|
|
347
|
+
logger.exception("get_trust_config failed")
|
|
348
|
+
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# PUT /api/v3/trust/config
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@router.put("/trust/config")
|
|
357
|
+
async def put_trust_config(request: Request, body: TrustConfigUpdate):
|
|
358
|
+
"""Update trust configuration.
|
|
359
|
+
|
|
360
|
+
Each field is stored in its canonical config.json sub-section.
|
|
361
|
+
Unrelated keys in those sub-sections are preserved.
|
|
362
|
+
"""
|
|
363
|
+
_require_admin(request)
|
|
364
|
+
try:
|
|
365
|
+
data = _read_config()
|
|
366
|
+
if body.use_trust_weighting is not None:
|
|
367
|
+
retrieval = data.setdefault("retrieval", {})
|
|
368
|
+
retrieval["use_trust_weighting"] = body.use_trust_weighting
|
|
369
|
+
if body.trust_first_party is not None:
|
|
370
|
+
injection = data.setdefault("injection", {})
|
|
371
|
+
injection["trust_first_party"] = body.trust_first_party
|
|
372
|
+
if body.promotion_min_trust is not None:
|
|
373
|
+
consolidation = data.setdefault("consolidation", {})
|
|
374
|
+
consolidation["promotion_min_trust"] = body.promotion_min_trust
|
|
375
|
+
_atomic_write(data)
|
|
376
|
+
retrieval = data.get("retrieval", {})
|
|
377
|
+
injection = data.get("injection", {})
|
|
378
|
+
consolidation = data.get("consolidation", {})
|
|
379
|
+
return {
|
|
380
|
+
"use_trust_weighting": retrieval.get("use_trust_weighting", True),
|
|
381
|
+
"trust_first_party": injection.get("trust_first_party", False),
|
|
382
|
+
"promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
|
|
383
|
+
}
|
|
384
|
+
except Exception as exc:
|
|
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
|
+
async 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 as exc:
|
|
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
|
+
async 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 on the next daemon startup (or after
|
|
420
|
+
the scheduler fires its next cycle).
|
|
421
|
+
"""
|
|
422
|
+
_require_admin(request)
|
|
423
|
+
try:
|
|
424
|
+
data = _read_config()
|
|
425
|
+
stored = data.get("forgetting", {})
|
|
426
|
+
# Apply defaults for missing fields, then overlay the stored values,
|
|
427
|
+
# then overlay the requested updates.
|
|
428
|
+
merged = {**_FORGETTING_DEFAULTS, **stored}
|
|
429
|
+
updates = body.model_dump(exclude_none=True)
|
|
430
|
+
merged.update(updates)
|
|
431
|
+
data["forgetting"] = merged
|
|
432
|
+
_atomic_write(data)
|
|
433
|
+
return {k: merged[k] for k in _FORGETTING_DEFAULTS}
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
logger.exception("put_forgetting_config failed")
|
|
436
|
+
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")
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from fastapi import APIRouter, HTTPException, Request, Query
|
|
10
10
|
|
|
11
|
-
from .helpers import require_engine
|
|
11
|
+
from .helpers import require_engine, get_active_profile
|
|
12
12
|
|
|
13
13
|
router = APIRouter(prefix="/api/entity", tags=["entity"])
|
|
14
14
|
|
|
@@ -16,12 +16,15 @@ router = APIRouter(prefix="/api/entity", tags=["entity"])
|
|
|
16
16
|
@router.get("/list")
|
|
17
17
|
async def list_entities(
|
|
18
18
|
request: Request,
|
|
19
|
-
profile: str = Query(default=
|
|
19
|
+
profile: str | None = Query(default=None),
|
|
20
20
|
limit: int = Query(default=100, ge=1, le=1000),
|
|
21
21
|
offset: int = Query(default=0, ge=0),
|
|
22
22
|
):
|
|
23
23
|
"""List all entities with basic info (canonical name, type, fact count)."""
|
|
24
24
|
engine = require_engine(request)
|
|
25
|
+
# Default to the ACTIVE profile (request runtime truth), never the literal
|
|
26
|
+
# "default" — otherwise every profile sees the default profile's entities.
|
|
27
|
+
profile = profile or get_active_profile()
|
|
25
28
|
|
|
26
29
|
import sqlite3
|
|
27
30
|
import json
|
|
@@ -71,11 +74,12 @@ async def list_entities(
|
|
|
71
74
|
async def get_entity(
|
|
72
75
|
entity_name: str,
|
|
73
76
|
request: Request,
|
|
74
|
-
profile: str = Query(default=
|
|
77
|
+
profile: str | None = Query(default=None),
|
|
75
78
|
project: str = Query(default=""),
|
|
76
79
|
):
|
|
77
80
|
"""Get compiled truth + timeline for an entity."""
|
|
78
81
|
engine = require_engine(request)
|
|
82
|
+
profile = profile or get_active_profile()
|
|
79
83
|
|
|
80
84
|
import sqlite3
|
|
81
85
|
import json
|
|
@@ -115,11 +119,12 @@ async def get_entity(
|
|
|
115
119
|
async def recompile_entity(
|
|
116
120
|
entity_name: str,
|
|
117
121
|
request: Request,
|
|
118
|
-
profile: str = Query(default=
|
|
122
|
+
profile: str | None = Query(default=None),
|
|
119
123
|
project: str = Query(default=""),
|
|
120
124
|
):
|
|
121
125
|
"""Force immediate recompilation of an entity."""
|
|
122
126
|
engine = require_engine(request)
|
|
127
|
+
profile = profile or get_active_profile()
|
|
123
128
|
|
|
124
129
|
import sqlite3
|
|
125
130
|
conn = sqlite3.connect(str(engine._config.db_path))
|
|
@@ -17,7 +17,7 @@ from datetime import datetime, timezone
|
|
|
17
17
|
from fastapi import APIRouter, HTTPException, Query
|
|
18
18
|
from fastapi.responses import StreamingResponse
|
|
19
19
|
|
|
20
|
-
from .helpers import DB_PATH
|
|
20
|
+
from .helpers import DB_PATH, get_active_profile
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger("superlocalmemory.routes.events")
|
|
23
23
|
router = APIRouter()
|
|
@@ -71,6 +71,11 @@ async def event_stream(
|
|
|
71
71
|
|
|
72
72
|
import asyncio
|
|
73
73
|
|
|
74
|
+
# Scope this stream to the profile active at connect time. The dashboard
|
|
75
|
+
# opens a fresh stream after a profile switch, so a connection only ever
|
|
76
|
+
# surfaces ONE profile's events — never another profile's (GDPR).
|
|
77
|
+
active_profile = get_active_profile()
|
|
78
|
+
|
|
74
79
|
client_queue = _queue.Queue(maxsize=100)
|
|
75
80
|
with _sse_queues_lock:
|
|
76
81
|
_sse_queues.add(client_queue)
|
|
@@ -86,6 +91,7 @@ async def event_stream(
|
|
|
86
91
|
bus = EventBus.get_instance(DB_PATH)
|
|
87
92
|
missed = bus.get_recent_events(
|
|
88
93
|
since_id=last_event_id, limit=50, event_type=event_type,
|
|
94
|
+
profile_id=active_profile,
|
|
89
95
|
)
|
|
90
96
|
for evt in missed:
|
|
91
97
|
data = json.dumps(evt)
|
|
@@ -96,7 +102,9 @@ async def event_stream(
|
|
|
96
102
|
else:
|
|
97
103
|
try:
|
|
98
104
|
bus = EventBus.get_instance(DB_PATH)
|
|
99
|
-
recent = bus.get_recent_events(
|
|
105
|
+
recent = bus.get_recent_events(
|
|
106
|
+
limit=1, profile_id=active_profile,
|
|
107
|
+
)
|
|
100
108
|
if recent:
|
|
101
109
|
last_db_id = recent[-1].get('id', 0)
|
|
102
110
|
except Exception:
|
|
@@ -110,6 +118,9 @@ async def event_stream(
|
|
|
110
118
|
event = client_queue.get_nowait()
|
|
111
119
|
if event_type and event.get("event_type") != event_type:
|
|
112
120
|
continue
|
|
121
|
+
# Profile isolation: never forward another profile's event.
|
|
122
|
+
if event.get("profile_id", "default") != active_profile:
|
|
123
|
+
continue
|
|
113
124
|
data = json.dumps(event)
|
|
114
125
|
event_id = event.get("id", event.get("seq", ""))
|
|
115
126
|
yield f"id: {event_id}\nevent: {event['event_type']}\ndata: {data}\n\n"
|
|
@@ -126,6 +137,7 @@ async def event_stream(
|
|
|
126
137
|
bus = EventBus.get_instance(DB_PATH)
|
|
127
138
|
new_events = bus.get_recent_events(
|
|
128
139
|
since_id=last_db_id, limit=10, event_type=event_type,
|
|
140
|
+
profile_id=active_profile,
|
|
129
141
|
)
|
|
130
142
|
for evt in new_events:
|
|
131
143
|
data = json.dumps(evt)
|
|
@@ -162,13 +174,16 @@ async def get_events(
|
|
|
162
174
|
return {"events": [], "count": 0, "message": "Event Bus not available"}
|
|
163
175
|
try:
|
|
164
176
|
bus = EventBus.get_instance(DB_PATH)
|
|
177
|
+
active_profile = get_active_profile()
|
|
165
178
|
events = bus.get_recent_events(
|
|
166
179
|
since_id=since_id, limit=limit, event_type=event_type,
|
|
180
|
+
profile_id=active_profile,
|
|
167
181
|
)
|
|
168
|
-
stats = bus.get_event_stats()
|
|
182
|
+
stats = bus.get_event_stats(profile_id=active_profile)
|
|
169
183
|
return {"events": events, "count": len(events), "stats": stats}
|
|
170
|
-
except Exception
|
|
171
|
-
|
|
184
|
+
except Exception:
|
|
185
|
+
logger.exception("events route error")
|
|
186
|
+
raise HTTPException(status_code=500, detail="Event retrieval error")
|
|
172
187
|
|
|
173
188
|
|
|
174
189
|
@router.get("/api/events/stats")
|
|
@@ -178,6 +193,7 @@ async def get_event_stats():
|
|
|
178
193
|
return {"total_events": 0, "message": "Event Bus not available"}
|
|
179
194
|
try:
|
|
180
195
|
bus = EventBus.get_instance(DB_PATH)
|
|
181
|
-
return bus.get_event_stats()
|
|
182
|
-
except Exception
|
|
183
|
-
|
|
196
|
+
return bus.get_event_stats(profile_id=get_active_profile())
|
|
197
|
+
except Exception:
|
|
198
|
+
logger.exception("events route error")
|
|
199
|
+
raise HTTPException(status_code=500, detail="Event stats error")
|