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
|
@@ -6,45 +6,117 @@
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from fastapi import APIRouter, HTTPException,
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
10
10
|
|
|
11
|
-
from .helpers import require_engine
|
|
11
|
+
from .helpers import get_active_profile, require_engine
|
|
12
12
|
|
|
13
13
|
router = APIRouter(prefix="/api/entity", tags=["entity"])
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _list_entities_sql(where_sql: str) -> str:
|
|
17
|
+
"""Build the page-first entity query after ``where_sql`` is parameterized."""
|
|
18
|
+
return f"""
|
|
19
|
+
WITH page_entities AS MATERIALIZED (
|
|
20
|
+
SELECT ce.entity_id, ce.profile_id, ce.canonical_name,
|
|
21
|
+
ce.entity_type, ce.fact_count, ce.first_seen, ce.last_seen
|
|
22
|
+
FROM canonical_entities ce
|
|
23
|
+
WHERE {where_sql}
|
|
24
|
+
ORDER BY ce.fact_count DESC, ce.entity_id ASC
|
|
25
|
+
LIMIT ? OFFSET ?
|
|
26
|
+
),
|
|
27
|
+
ranked_profiles AS MATERIALIZED (
|
|
28
|
+
SELECT ep.*,
|
|
29
|
+
ROW_NUMBER() OVER (
|
|
30
|
+
PARTITION BY ep.entity_id, ep.profile_id
|
|
31
|
+
ORDER BY COALESCE(ep.last_compiled_at, '') DESC,
|
|
32
|
+
ep.project_name COLLATE NOCASE ASC,
|
|
33
|
+
ep.rowid ASC
|
|
34
|
+
) AS summary_rank
|
|
35
|
+
FROM entity_profiles ep
|
|
36
|
+
JOIN page_entities page
|
|
37
|
+
ON page.entity_id = ep.entity_id
|
|
38
|
+
AND page.profile_id = ep.profile_id
|
|
39
|
+
WHERE ep.profile_id = ?
|
|
40
|
+
)
|
|
41
|
+
SELECT ce.entity_id, ce.canonical_name, ce.entity_type,
|
|
42
|
+
ce.fact_count, ce.first_seen, ce.last_seen,
|
|
43
|
+
ep.knowledge_summary, ep.compiled_truth,
|
|
44
|
+
ep.compilation_confidence, ep.last_compiled_at
|
|
45
|
+
FROM page_entities ce
|
|
46
|
+
LEFT JOIN ranked_profiles ep
|
|
47
|
+
ON ce.entity_id = ep.entity_id
|
|
48
|
+
AND ep.profile_id = ce.profile_id
|
|
49
|
+
AND ep.summary_rank = 1
|
|
50
|
+
ORDER BY ce.fact_count DESC, ce.entity_id ASC
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _require_read(request: Request, profile: str) -> None:
|
|
55
|
+
"""Authorize entity metadata access for the explicitly requested profile."""
|
|
56
|
+
from superlocalmemory.access.rbac import Permission
|
|
57
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
58
|
+
|
|
59
|
+
require_permission(request, Permission.READ, profile=profile)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _require_manage(request: Request, profile: str) -> None:
|
|
63
|
+
"""Authorize entity recompilation for the explicitly requested profile."""
|
|
64
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
65
|
+
|
|
66
|
+
require_manage(request, profile=profile)
|
|
67
|
+
|
|
68
|
+
|
|
16
69
|
@router.get("/list")
|
|
17
|
-
|
|
70
|
+
def list_entities(
|
|
18
71
|
request: Request,
|
|
19
|
-
profile: str = Query(default=
|
|
72
|
+
profile: str | None = Query(default=None),
|
|
73
|
+
entity_type: str | None = Query(default=None, alias="type", max_length=80),
|
|
74
|
+
search: str | None = Query(default=None, max_length=200),
|
|
20
75
|
limit: int = Query(default=100, ge=1, le=1000),
|
|
21
76
|
offset: int = Query(default=0, ge=0),
|
|
22
77
|
):
|
|
23
|
-
"""List
|
|
78
|
+
"""List a profile's entities, filtering before count and pagination."""
|
|
24
79
|
engine = require_engine(request)
|
|
80
|
+
# Default to the ACTIVE profile (request runtime truth), never the literal
|
|
81
|
+
# "default" — otherwise every profile sees the default profile's entities.
|
|
82
|
+
profile = profile or get_active_profile()
|
|
83
|
+
_require_read(request, profile)
|
|
25
84
|
|
|
26
85
|
import sqlite3
|
|
27
|
-
import json
|
|
28
86
|
conn = sqlite3.connect(str(engine._config.db_path))
|
|
29
87
|
conn.row_factory = sqlite3.Row
|
|
30
88
|
try:
|
|
89
|
+
where = ["ce.profile_id = ?"]
|
|
90
|
+
params: list[object] = [profile]
|
|
91
|
+
if entity_type and entity_type.lower() != "all":
|
|
92
|
+
where.append("ce.entity_type = ? COLLATE NOCASE")
|
|
93
|
+
params.append(entity_type.strip().lower())
|
|
94
|
+
if search and search.strip():
|
|
95
|
+
escaped = (search.strip().lower().replace("\\", "\\\\")
|
|
96
|
+
.replace("%", "\\%").replace("_", "\\_"))
|
|
97
|
+
where.append(
|
|
98
|
+
"(LOWER(ce.canonical_name) LIKE ? ESCAPE '\\' "
|
|
99
|
+
"OR LOWER(COALESCE(ce.entity_type, 'unknown')) LIKE ? ESCAPE '\\' "
|
|
100
|
+
"OR EXISTS ("
|
|
101
|
+
"SELECT 1 FROM entity_profiles eps "
|
|
102
|
+
"WHERE eps.entity_id = ce.entity_id "
|
|
103
|
+
"AND eps.profile_id = ce.profile_id "
|
|
104
|
+
"AND LOWER(COALESCE(eps.knowledge_summary, '')) "
|
|
105
|
+
"LIKE ? ESCAPE '\\'))"
|
|
106
|
+
)
|
|
107
|
+
params.extend([f"%{escaped}%"] * 3)
|
|
108
|
+
where_sql = " AND ".join(where)
|
|
109
|
+
|
|
31
110
|
total = conn.execute(
|
|
32
|
-
"SELECT COUNT(*) FROM canonical_entities
|
|
33
|
-
|
|
111
|
+
"SELECT COUNT(*) FROM canonical_entities ce "
|
|
112
|
+
f"WHERE {where_sql}",
|
|
113
|
+
params,
|
|
34
114
|
).fetchone()[0]
|
|
35
115
|
|
|
36
|
-
rows = conn.execute(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ep.compilation_confidence, ep.last_compiled_at
|
|
41
|
-
FROM canonical_entities ce
|
|
42
|
-
LEFT JOIN entity_profiles ep
|
|
43
|
-
ON ce.entity_id = ep.entity_id AND ep.profile_id = ce.profile_id
|
|
44
|
-
WHERE ce.profile_id = ?
|
|
45
|
-
ORDER BY ce.fact_count DESC
|
|
46
|
-
LIMIT ? OFFSET ?
|
|
47
|
-
""", (profile, limit, offset)).fetchall()
|
|
116
|
+
rows = conn.execute(
|
|
117
|
+
_list_entities_sql(where_sql),
|
|
118
|
+
[*params, limit, offset, profile],
|
|
119
|
+
).fetchall()
|
|
48
120
|
|
|
49
121
|
entities = []
|
|
50
122
|
for r in rows:
|
|
@@ -62,23 +134,31 @@ async def list_entities(
|
|
|
62
134
|
"last_compiled_at": r["last_compiled_at"],
|
|
63
135
|
})
|
|
64
136
|
|
|
65
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
"entities": entities,
|
|
139
|
+
"total": total,
|
|
140
|
+
"limit": limit,
|
|
141
|
+
"offset": offset,
|
|
142
|
+
"has_more": offset + limit < total,
|
|
143
|
+
}
|
|
66
144
|
finally:
|
|
67
145
|
conn.close()
|
|
68
146
|
|
|
69
147
|
|
|
70
148
|
@router.get("/{entity_name}")
|
|
71
|
-
|
|
149
|
+
def get_entity(
|
|
72
150
|
entity_name: str,
|
|
73
151
|
request: Request,
|
|
74
|
-
profile: str = Query(default=
|
|
152
|
+
profile: str | None = Query(default=None),
|
|
75
153
|
project: str = Query(default=""),
|
|
76
154
|
):
|
|
77
155
|
"""Get compiled truth + timeline for an entity."""
|
|
78
156
|
engine = require_engine(request)
|
|
157
|
+
profile = profile or get_active_profile()
|
|
158
|
+
_require_read(request, profile)
|
|
79
159
|
|
|
80
|
-
import sqlite3
|
|
81
160
|
import json
|
|
161
|
+
import sqlite3
|
|
82
162
|
conn = sqlite3.connect(str(engine._config.db_path))
|
|
83
163
|
conn.row_factory = sqlite3.Row
|
|
84
164
|
try:
|
|
@@ -112,14 +192,16 @@ async def get_entity(
|
|
|
112
192
|
|
|
113
193
|
|
|
114
194
|
@router.post("/{entity_name}/recompile")
|
|
115
|
-
|
|
195
|
+
def recompile_entity(
|
|
116
196
|
entity_name: str,
|
|
117
197
|
request: Request,
|
|
118
|
-
profile: str = Query(default=
|
|
198
|
+
profile: str | None = Query(default=None),
|
|
119
199
|
project: str = Query(default=""),
|
|
120
200
|
):
|
|
121
201
|
"""Force immediate recompilation of an entity."""
|
|
122
202
|
engine = require_engine(request)
|
|
203
|
+
profile = profile or get_active_profile()
|
|
204
|
+
_require_manage(request, profile)
|
|
123
205
|
|
|
124
206
|
import sqlite3
|
|
125
207
|
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")
|
|
@@ -8,46 +8,89 @@ Routes: /api/evolution/status, /api/evolution/enable, /api/evolution/run
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
-
from
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
from typing import Optional
|
|
12
13
|
|
|
13
|
-
from fastapi import APIRouter
|
|
14
|
+
from fastapi import APIRouter, Request
|
|
15
|
+
from pydantic import BaseModel
|
|
14
16
|
|
|
15
|
-
from .
|
|
17
|
+
from superlocalmemory.server.config_file import read_config, update_config
|
|
18
|
+
|
|
19
|
+
from .helpers import MEMORY_DIR, get_active_profile
|
|
16
20
|
|
|
17
21
|
logger = logging.getLogger("superlocalmemory.routes.evolution")
|
|
18
22
|
router = APIRouter()
|
|
19
23
|
|
|
20
24
|
|
|
25
|
+
def _require_read(request: Request) -> None:
|
|
26
|
+
"""Guard evolution telemetry with READ on the active profile."""
|
|
27
|
+
from superlocalmemory.access.rbac import Permission
|
|
28
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
29
|
+
|
|
30
|
+
require_permission(request, Permission.READ, profile=get_active_profile())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _require_manage(request: Request) -> None:
|
|
34
|
+
"""Guard evolution mutations with the same RBAC boundary as v3 settings."""
|
|
35
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
36
|
+
|
|
37
|
+
require_manage(request)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_evolution_config() -> dict:
|
|
41
|
+
"""Read one process-safe evolution config snapshot."""
|
|
42
|
+
return dict(read_config(MEMORY_DIR / "config.json").get("evolution", {}))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _update_evolution_config(update) -> dict:
|
|
46
|
+
"""Atomically update evolution without losing other config sections."""
|
|
47
|
+
config_path = MEMORY_DIR / "config.json"
|
|
48
|
+
|
|
49
|
+
def mutate(cfg: dict) -> None:
|
|
50
|
+
evolution = cfg.setdefault("evolution", {})
|
|
51
|
+
update(evolution)
|
|
52
|
+
|
|
53
|
+
cfg = update_config(config_path, mutate)
|
|
54
|
+
return dict(cfg.get("evolution", {}))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _enable_evolution(config: dict) -> None:
|
|
58
|
+
config["enabled"] = True
|
|
59
|
+
config.setdefault("backend", "auto")
|
|
60
|
+
|
|
61
|
+
|
|
21
62
|
@router.get("/api/evolution/status")
|
|
22
|
-
|
|
63
|
+
def evolution_status(request: Request):
|
|
23
64
|
"""Get evolution engine status, backend, and recent history."""
|
|
65
|
+
_require_read(request)
|
|
24
66
|
try:
|
|
25
|
-
import json as _json
|
|
26
|
-
from superlocalmemory.evolution.skill_evolver import detect_backend
|
|
27
67
|
from superlocalmemory.evolution.evolution_store import EvolutionStore
|
|
68
|
+
from superlocalmemory.evolution.skill_evolver import detect_backend
|
|
28
69
|
|
|
29
|
-
|
|
30
|
-
config_path = MEMORY_DIR / "config.json"
|
|
31
|
-
evo_cfg = {}
|
|
32
|
-
if config_path.exists():
|
|
33
|
-
with open(config_path) as f:
|
|
34
|
-
cfg = _json.load(f)
|
|
35
|
-
evo_cfg = cfg.get("evolution", {})
|
|
36
|
-
|
|
70
|
+
evo_cfg = _read_evolution_config()
|
|
37
71
|
enabled = evo_cfg.get("enabled", False)
|
|
38
|
-
|
|
72
|
+
backend_setting = evo_cfg.get("backend", "auto")
|
|
73
|
+
backend = (
|
|
74
|
+
detect_backend() if enabled and backend_setting == "auto"
|
|
75
|
+
else backend_setting if enabled
|
|
76
|
+
else "none"
|
|
77
|
+
)
|
|
39
78
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
40
79
|
|
|
80
|
+
profile_id = get_active_profile()
|
|
41
81
|
store = EvolutionStore(db_path)
|
|
42
|
-
stats = store.get_stats()
|
|
43
|
-
recent = store.get_recent(limit=10)
|
|
82
|
+
stats = store.get_stats(profile_id)
|
|
83
|
+
recent = store.get_recent(profile_id, limit=10)
|
|
44
84
|
|
|
45
85
|
return {
|
|
46
86
|
"enabled": enabled,
|
|
47
87
|
"backend": backend,
|
|
48
88
|
"config": {
|
|
49
|
-
"backend_setting":
|
|
89
|
+
"backend_setting": backend_setting,
|
|
50
90
|
"max_per_cycle": evo_cfg.get("max_evolutions_per_cycle", 3),
|
|
91
|
+
"mutation_model": evo_cfg.get("mutation_model", ""),
|
|
92
|
+
"verify_model": evo_cfg.get("verify_model", ""),
|
|
93
|
+
"confirm_model": evo_cfg.get("confirm_model", ""),
|
|
51
94
|
},
|
|
52
95
|
"stats": {
|
|
53
96
|
"total": stats.get("total", 0),
|
|
@@ -70,83 +113,158 @@ async def evolution_status():
|
|
|
70
113
|
for r in recent
|
|
71
114
|
],
|
|
72
115
|
}
|
|
73
|
-
except Exception
|
|
74
|
-
logger.
|
|
75
|
-
return {"enabled": False, "backend": "none", "error":
|
|
116
|
+
except Exception:
|
|
117
|
+
logger.exception("evolution_status error")
|
|
118
|
+
return {"enabled": False, "backend": "none", "error": "Internal server error"}
|
|
76
119
|
|
|
77
120
|
|
|
78
121
|
@router.post("/api/evolution/enable")
|
|
79
|
-
|
|
80
|
-
"""Enable
|
|
122
|
+
def evolution_enable(request: Request):
|
|
123
|
+
"""Enable evolution without replacing the user's selected backend."""
|
|
124
|
+
_require_manage(request)
|
|
81
125
|
try:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
126
|
+
evolution = _update_evolution_config(_enable_evolution)
|
|
127
|
+
return {
|
|
128
|
+
"ok": True,
|
|
129
|
+
"message": f"Evolution enabled with {evolution['backend']} backend.",
|
|
130
|
+
}
|
|
131
|
+
except Exception:
|
|
132
|
+
logger.exception("evolution_enable error")
|
|
133
|
+
return {"ok": False, "error": "Internal server error"}
|
|
89
134
|
|
|
90
|
-
if "evolution" not in cfg:
|
|
91
|
-
cfg["evolution"] = {}
|
|
92
|
-
cfg["evolution"]["enabled"] = True
|
|
93
|
-
cfg["evolution"]["backend"] = "auto"
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
@router.post("/api/evolution/disable")
|
|
137
|
+
def evolution_disable(request: Request):
|
|
138
|
+
"""Disable skill evolution engine. Mirrors /api/evolution/enable."""
|
|
139
|
+
_require_manage(request)
|
|
140
|
+
try:
|
|
141
|
+
_update_evolution_config(lambda cfg: cfg.update({"enabled": False}))
|
|
97
142
|
|
|
98
|
-
return {"ok": True, "message": "Evolution
|
|
99
|
-
except Exception
|
|
100
|
-
logger.
|
|
101
|
-
return {"ok": False, "error":
|
|
143
|
+
return {"ok": True, "message": "Evolution disabled."}
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.exception("evolution_disable error")
|
|
146
|
+
return {"ok": False, "error": "Internal server error"}
|
|
102
147
|
|
|
103
148
|
|
|
104
149
|
@router.post("/api/evolution/run")
|
|
105
|
-
|
|
150
|
+
def evolution_run(request: Request):
|
|
106
151
|
"""Manually trigger an evolution cycle."""
|
|
152
|
+
_require_manage(request)
|
|
107
153
|
try:
|
|
108
|
-
import json as _json
|
|
109
154
|
from superlocalmemory.evolution.skill_evolver import SkillEvolver
|
|
110
155
|
|
|
111
|
-
|
|
112
|
-
evo_cfg = {}
|
|
113
|
-
if config_path.exists():
|
|
114
|
-
with open(config_path) as f:
|
|
115
|
-
evo_cfg = _json.load(f).get("evolution", {})
|
|
116
|
-
|
|
156
|
+
evo_cfg = _read_evolution_config()
|
|
117
157
|
if not evo_cfg.get("enabled", False):
|
|
118
158
|
return {"ok": False, "error": "Evolution is disabled. Enable first."}
|
|
119
159
|
|
|
120
160
|
profile = get_active_profile()
|
|
121
161
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
122
162
|
|
|
123
|
-
# Build a minimal config object for the evolver
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
# Build a minimal config object for the evolver. Must carry the
|
|
164
|
+
# per-step model fields (v3.7.9) or a dashboard-triggered run would
|
|
165
|
+
# silently ignore the user's configured models and fall back to the
|
|
166
|
+
# cheapest defaults.
|
|
167
|
+
evolution_config = SimpleNamespace(
|
|
168
|
+
enabled=True,
|
|
169
|
+
backend=evo_cfg.get("backend", "auto"),
|
|
170
|
+
max_evolutions_per_cycle=evo_cfg.get("max_evolutions_per_cycle", 3),
|
|
171
|
+
mutation_model=evo_cfg.get("mutation_model", ""),
|
|
172
|
+
verify_model=evo_cfg.get("verify_model", ""),
|
|
173
|
+
confirm_model=evo_cfg.get("confirm_model", ""),
|
|
174
|
+
)
|
|
175
|
+
evolver = SkillEvolver(
|
|
176
|
+
db_path, SimpleNamespace(evolution=evolution_config)
|
|
177
|
+
)
|
|
132
178
|
result = evolver.run_consolidation_cycle(profile)
|
|
133
179
|
|
|
134
180
|
return {"ok": True, **result}
|
|
135
|
-
except Exception
|
|
136
|
-
logger.
|
|
137
|
-
return {"ok": False, "error":
|
|
181
|
+
except Exception:
|
|
182
|
+
logger.exception("evolution_run error")
|
|
183
|
+
return {"ok": False, "error": "Internal server error"}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class EvolutionConfigUpdate(BaseModel):
|
|
187
|
+
enabled: Optional[bool] = None
|
|
188
|
+
backend: Optional[str] = None
|
|
189
|
+
max_evolutions_per_cycle: Optional[int] = None
|
|
190
|
+
mutation_model: Optional[str] = None
|
|
191
|
+
verify_model: Optional[str] = None
|
|
192
|
+
confirm_model: Optional[str] = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.post("/api/evolution/config")
|
|
196
|
+
def evolution_config(request: Request, body: EvolutionConfigUpdate):
|
|
197
|
+
"""Update evolution config from the dashboard (v3.7.9).
|
|
198
|
+
|
|
199
|
+
Validates model + backend values against the same allow-list the CLI uses
|
|
200
|
+
(``slm config set evolution.*``) and persists to config.json atomically.
|
|
201
|
+
Only fields provided in the body are changed.
|
|
202
|
+
"""
|
|
203
|
+
_require_manage(request)
|
|
204
|
+
try:
|
|
205
|
+
from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
|
|
206
|
+
|
|
207
|
+
accepted_models = set(_MODEL_ALIASES) | {"", "auto"}
|
|
208
|
+
accepted_backends = {"auto", "claude", "ollama", "anthropic", "openai"}
|
|
209
|
+
|
|
210
|
+
for field in ("mutation_model", "verify_model", "confirm_model"):
|
|
211
|
+
val = getattr(body, field)
|
|
212
|
+
if val is not None and val not in accepted_models:
|
|
213
|
+
allowed = ", ".join(["auto", *sorted(_MODEL_ALIASES)])
|
|
214
|
+
return {"ok": False, "error": f"{field} must be one of: {allowed}"}
|
|
215
|
+
if body.backend is not None and body.backend not in accepted_backends:
|
|
216
|
+
return {
|
|
217
|
+
"ok": False,
|
|
218
|
+
"error": f"backend must be one of: {', '.join(sorted(accepted_backends))}",
|
|
219
|
+
}
|
|
220
|
+
if (body.max_evolutions_per_cycle is not None
|
|
221
|
+
and not 0 < body.max_evolutions_per_cycle <= 50):
|
|
222
|
+
return {"ok": False, "error": "max_evolutions_per_cycle must be 1..50"}
|
|
223
|
+
|
|
224
|
+
def _apply(evo: dict) -> None:
|
|
225
|
+
for field in (
|
|
226
|
+
"enabled",
|
|
227
|
+
"backend",
|
|
228
|
+
"max_evolutions_per_cycle",
|
|
229
|
+
"mutation_model",
|
|
230
|
+
"verify_model",
|
|
231
|
+
"confirm_model",
|
|
232
|
+
):
|
|
233
|
+
value = getattr(body, field)
|
|
234
|
+
if value is None:
|
|
235
|
+
continue
|
|
236
|
+
if field.endswith("_model") and value == "auto":
|
|
237
|
+
value = ""
|
|
238
|
+
evo[field] = value
|
|
239
|
+
|
|
240
|
+
evo = _update_evolution_config(_apply)
|
|
241
|
+
|
|
242
|
+
return {"ok": True, "config": {
|
|
243
|
+
"enabled": evo.get("enabled", False),
|
|
244
|
+
"backend": evo.get("backend", "auto"),
|
|
245
|
+
"max_evolutions_per_cycle": evo.get("max_evolutions_per_cycle", 3),
|
|
246
|
+
"mutation_model": evo.get("mutation_model", ""),
|
|
247
|
+
"verify_model": evo.get("verify_model", ""),
|
|
248
|
+
"confirm_model": evo.get("confirm_model", ""),
|
|
249
|
+
}}
|
|
250
|
+
except Exception:
|
|
251
|
+
logger.exception("evolution_config error")
|
|
252
|
+
return {"ok": False, "error": "Internal server error"}
|
|
138
253
|
|
|
139
254
|
|
|
140
255
|
@router.get("/api/evolution/lineage")
|
|
141
|
-
|
|
256
|
+
def evolution_lineage(request: Request, skill_name: str = ""):
|
|
142
257
|
"""Get evolution lineage for a skill or all skills.
|
|
143
258
|
|
|
144
259
|
Returns lineage records and a tree structure grouped by root skill.
|
|
145
260
|
"""
|
|
261
|
+
_require_read(request)
|
|
262
|
+
conn = None
|
|
146
263
|
try:
|
|
147
264
|
import sqlite3 as _sqlite3
|
|
148
265
|
|
|
149
266
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
267
|
+
profile_id = get_active_profile()
|
|
150
268
|
conn = _sqlite3.connect(db_path, timeout=10)
|
|
151
269
|
conn.row_factory = _sqlite3.Row
|
|
152
270
|
|
|
@@ -156,9 +274,9 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
156
274
|
"trigger_type, generation, status, mutation_summary, "
|
|
157
275
|
"blind_verified, created_at, completed_at "
|
|
158
276
|
"FROM skill_evolution_log "
|
|
159
|
-
"WHERE skill_name = ? OR parent_skill_id = ? "
|
|
277
|
+
"WHERE profile_id = ? AND (skill_name = ? OR parent_skill_id = ?) "
|
|
160
278
|
"ORDER BY created_at ASC",
|
|
161
|
-
(skill_name, skill_name),
|
|
279
|
+
(profile_id, skill_name, skill_name),
|
|
162
280
|
).fetchall()
|
|
163
281
|
else:
|
|
164
282
|
rows = conn.execute(
|
|
@@ -166,11 +284,11 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
166
284
|
"trigger_type, generation, status, mutation_summary, "
|
|
167
285
|
"blind_verified, created_at, completed_at "
|
|
168
286
|
"FROM skill_evolution_log "
|
|
287
|
+
"WHERE profile_id = ? "
|
|
169
288
|
"ORDER BY created_at DESC LIMIT 100",
|
|
289
|
+
(profile_id,),
|
|
170
290
|
).fetchall()
|
|
171
291
|
|
|
172
|
-
conn.close()
|
|
173
|
-
|
|
174
292
|
lineage = [
|
|
175
293
|
{
|
|
176
294
|
"id": dict(r)["id"],
|
|
@@ -208,6 +326,9 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
208
326
|
"lineage_count": len(lineage),
|
|
209
327
|
"tree": tree,
|
|
210
328
|
}
|
|
211
|
-
except Exception
|
|
212
|
-
logger.
|
|
213
|
-
return {"lineage": [], "lineage_count": 0, "tree": {}, "error":
|
|
329
|
+
except Exception:
|
|
330
|
+
logger.exception("evolution_lineage error")
|
|
331
|
+
return {"lineage": [], "lineage_count": 0, "tree": {}, "error": "Internal server error"}
|
|
332
|
+
finally:
|
|
333
|
+
if conn is not None:
|
|
334
|
+
conn.close()
|
|
@@ -342,12 +342,23 @@ def set_active_profile_everywhere(name: str) -> None:
|
|
|
342
342
|
|
|
343
343
|
|
|
344
344
|
def delete_profile_from_db(name: str) -> None:
|
|
345
|
-
"""Delete a profile row from SQLite.
|
|
345
|
+
"""Delete a profile row from SQLite.
|
|
346
|
+
|
|
347
|
+
rbac_memberships has no FK to profiles, so CASCADE does not remove role
|
|
348
|
+
grants — they would otherwise survive deletion and silently re-activate if
|
|
349
|
+
a profile of the same name is later recreated. Remove them explicitly.
|
|
350
|
+
"""
|
|
346
351
|
if not DB_PATH.exists():
|
|
347
352
|
return
|
|
348
353
|
conn = sqlite3.connect(str(DB_PATH))
|
|
349
354
|
try:
|
|
350
355
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
356
|
+
# Purge role grants for this workspace (no FK CASCADE covers these).
|
|
357
|
+
for tbl in ("rbac_memberships",):
|
|
358
|
+
try:
|
|
359
|
+
conn.execute(f"DELETE FROM {tbl} WHERE profile_id = ?", (name,))
|
|
360
|
+
except sqlite3.OperationalError:
|
|
361
|
+
pass # table may not exist on older installs
|
|
351
362
|
conn.execute("DELETE FROM profiles WHERE profile_id = ?", (name,))
|
|
352
363
|
conn.commit()
|
|
353
364
|
finally:
|
|
@@ -420,6 +431,10 @@ class SearchRequest(BaseModel):
|
|
|
420
431
|
cluster_id: Optional[int] = None
|
|
421
432
|
date_from: Optional[str] = None
|
|
422
433
|
date_to: Optional[str] = None
|
|
434
|
+
# T-window: relative span ("7d", "30d", "1y") or explicit range
|
|
435
|
+
# ("2026-07-01..2026-07-31"). When empty, date_from/date_to (if both set)
|
|
436
|
+
# are used as the range. Empty + no dates = no time filter.
|
|
437
|
+
window: Optional[str] = None
|
|
423
438
|
|
|
424
439
|
|
|
425
440
|
class ProfileSwitch(BaseModel):
|