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
|
@@ -9,8 +9,10 @@ Routes: /api/evolution/status, /api/evolution/enable, /api/evolution/run
|
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
11
|
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
12
13
|
|
|
13
14
|
from fastapi import APIRouter
|
|
15
|
+
from pydantic import BaseModel
|
|
14
16
|
|
|
15
17
|
from .helpers import get_active_profile, MEMORY_DIR
|
|
16
18
|
|
|
@@ -38,9 +40,10 @@ async def evolution_status():
|
|
|
38
40
|
backend = detect_backend() if enabled else "none"
|
|
39
41
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
40
42
|
|
|
43
|
+
profile_id = get_active_profile()
|
|
41
44
|
store = EvolutionStore(db_path)
|
|
42
|
-
stats = store.get_stats()
|
|
43
|
-
recent = store.get_recent(limit=10)
|
|
45
|
+
stats = store.get_stats(profile_id)
|
|
46
|
+
recent = store.get_recent(profile_id, limit=10)
|
|
44
47
|
|
|
45
48
|
return {
|
|
46
49
|
"enabled": enabled,
|
|
@@ -48,6 +51,9 @@ async def evolution_status():
|
|
|
48
51
|
"config": {
|
|
49
52
|
"backend_setting": evo_cfg.get("backend", "auto"),
|
|
50
53
|
"max_per_cycle": evo_cfg.get("max_evolutions_per_cycle", 3),
|
|
54
|
+
"mutation_model": evo_cfg.get("mutation_model", ""),
|
|
55
|
+
"verify_model": evo_cfg.get("verify_model", ""),
|
|
56
|
+
"confirm_model": evo_cfg.get("confirm_model", ""),
|
|
51
57
|
},
|
|
52
58
|
"stats": {
|
|
53
59
|
"total": stats.get("total", 0),
|
|
@@ -70,9 +76,9 @@ async def evolution_status():
|
|
|
70
76
|
for r in recent
|
|
71
77
|
],
|
|
72
78
|
}
|
|
73
|
-
except Exception
|
|
74
|
-
logger.
|
|
75
|
-
return {"enabled": False, "backend": "none", "error":
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.exception("evolution_status error")
|
|
81
|
+
return {"enabled": False, "backend": "none", "error": "Internal server error"}
|
|
76
82
|
|
|
77
83
|
|
|
78
84
|
@router.post("/api/evolution/enable")
|
|
@@ -96,9 +102,38 @@ async def evolution_enable():
|
|
|
96
102
|
_json.dump(cfg, f, indent=2)
|
|
97
103
|
|
|
98
104
|
return {"ok": True, "message": "Evolution enabled. Will use auto-detected backend."}
|
|
99
|
-
except Exception
|
|
100
|
-
logger.
|
|
101
|
-
return {"ok": False, "error":
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.exception("evolution_enable error")
|
|
107
|
+
return {"ok": False, "error": "Internal server error"}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@router.post("/api/evolution/disable")
|
|
111
|
+
async def evolution_disable():
|
|
112
|
+
"""Disable skill evolution engine. Mirrors /api/evolution/enable."""
|
|
113
|
+
try:
|
|
114
|
+
import json as _json
|
|
115
|
+
import os as _os
|
|
116
|
+
|
|
117
|
+
config_path = MEMORY_DIR / "config.json"
|
|
118
|
+
cfg: dict = {}
|
|
119
|
+
if config_path.exists():
|
|
120
|
+
with open(config_path) as f:
|
|
121
|
+
cfg = _json.load(f)
|
|
122
|
+
|
|
123
|
+
if "evolution" not in cfg:
|
|
124
|
+
cfg["evolution"] = {}
|
|
125
|
+
cfg["evolution"]["enabled"] = False
|
|
126
|
+
|
|
127
|
+
# Atomic write — protect against truncated config.json on crash.
|
|
128
|
+
tmp = config_path.with_suffix(".json.evo_disable_tmp")
|
|
129
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
tmp.write_text(_json.dumps(cfg, indent=2), encoding="utf-8")
|
|
131
|
+
_os.replace(tmp, config_path)
|
|
132
|
+
|
|
133
|
+
return {"ok": True, "message": "Evolution disabled."}
|
|
134
|
+
except Exception:
|
|
135
|
+
logger.exception("evolution_disable error")
|
|
136
|
+
return {"ok": False, "error": "Internal server error"}
|
|
102
137
|
|
|
103
138
|
|
|
104
139
|
@router.post("/api/evolution/run")
|
|
@@ -120,11 +155,17 @@ async def evolution_run():
|
|
|
120
155
|
profile = get_active_profile()
|
|
121
156
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
122
157
|
|
|
123
|
-
# Build a minimal config object for the evolver
|
|
158
|
+
# Build a minimal config object for the evolver. Must carry the
|
|
159
|
+
# per-step model fields (v3.7.9) or a dashboard-triggered run would
|
|
160
|
+
# silently ignore the user's configured models and fall back to the
|
|
161
|
+
# cheapest defaults.
|
|
124
162
|
class _EvoCfg:
|
|
125
163
|
enabled = True
|
|
126
164
|
backend = evo_cfg.get("backend", "auto")
|
|
127
165
|
max_evolutions_per_cycle = evo_cfg.get("max_evolutions_per_cycle", 3)
|
|
166
|
+
mutation_model = evo_cfg.get("mutation_model", "")
|
|
167
|
+
verify_model = evo_cfg.get("verify_model", "")
|
|
168
|
+
confirm_model = evo_cfg.get("confirm_model", "")
|
|
128
169
|
class _Cfg:
|
|
129
170
|
evolution = _EvoCfg()
|
|
130
171
|
|
|
@@ -132,9 +173,83 @@ async def evolution_run():
|
|
|
132
173
|
result = evolver.run_consolidation_cycle(profile)
|
|
133
174
|
|
|
134
175
|
return {"ok": True, **result}
|
|
135
|
-
except Exception
|
|
136
|
-
logger.
|
|
137
|
-
return {"ok": False, "error":
|
|
176
|
+
except Exception:
|
|
177
|
+
logger.exception("evolution_run error")
|
|
178
|
+
return {"ok": False, "error": "Internal server error"}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class EvolutionConfigUpdate(BaseModel):
|
|
182
|
+
enabled: Optional[bool] = None
|
|
183
|
+
backend: Optional[str] = None
|
|
184
|
+
max_evolutions_per_cycle: Optional[int] = None
|
|
185
|
+
mutation_model: Optional[str] = None
|
|
186
|
+
verify_model: Optional[str] = None
|
|
187
|
+
confirm_model: Optional[str] = None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.post("/api/evolution/config")
|
|
191
|
+
async def evolution_config(body: EvolutionConfigUpdate):
|
|
192
|
+
"""Update evolution config from the dashboard (v3.7.9).
|
|
193
|
+
|
|
194
|
+
Validates model + backend values against the same allow-list the CLI uses
|
|
195
|
+
(``slm config set evolution.*``) and persists to config.json atomically.
|
|
196
|
+
Only fields provided in the body are changed.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
import json as _json
|
|
200
|
+
import os as _os
|
|
201
|
+
from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
|
|
202
|
+
|
|
203
|
+
accepted_models = set(_MODEL_ALIASES) | {"", "auto"}
|
|
204
|
+
accepted_backends = {"auto", "claude", "ollama", "anthropic", "openai"}
|
|
205
|
+
|
|
206
|
+
for field in ("mutation_model", "verify_model", "confirm_model"):
|
|
207
|
+
val = getattr(body, field)
|
|
208
|
+
if val is not None and val not in accepted_models:
|
|
209
|
+
allowed = ", ".join(["auto", *sorted(_MODEL_ALIASES)])
|
|
210
|
+
return {"ok": False, "error": f"{field} must be one of: {allowed}"}
|
|
211
|
+
if body.backend is not None and body.backend not in accepted_backends:
|
|
212
|
+
return {
|
|
213
|
+
"ok": False,
|
|
214
|
+
"error": f"backend must be one of: {', '.join(sorted(accepted_backends))}",
|
|
215
|
+
}
|
|
216
|
+
if (body.max_evolutions_per_cycle is not None
|
|
217
|
+
and not 0 < body.max_evolutions_per_cycle <= 50):
|
|
218
|
+
return {"ok": False, "error": "max_evolutions_per_cycle must be 1..50"}
|
|
219
|
+
|
|
220
|
+
config_path = MEMORY_DIR / "config.json"
|
|
221
|
+
cfg = {}
|
|
222
|
+
if config_path.exists():
|
|
223
|
+
with open(config_path) as f:
|
|
224
|
+
cfg = _json.load(f)
|
|
225
|
+
evo = cfg.setdefault("evolution", {})
|
|
226
|
+
|
|
227
|
+
for field in ("enabled", "backend", "max_evolutions_per_cycle",
|
|
228
|
+
"mutation_model", "verify_model", "confirm_model"):
|
|
229
|
+
val = getattr(body, field)
|
|
230
|
+
if val is None:
|
|
231
|
+
continue
|
|
232
|
+
# "auto" is the UI label for the resolver's "" (pick cheapest model).
|
|
233
|
+
if field.endswith("_model") and val == "auto":
|
|
234
|
+
val = ""
|
|
235
|
+
evo[field] = val
|
|
236
|
+
|
|
237
|
+
# Atomic write — a truncated config.json would brick every slm call.
|
|
238
|
+
tmp = config_path.with_suffix(".json.evo_tmp")
|
|
239
|
+
tmp.write_text(_json.dumps(cfg, indent=2), encoding="utf-8")
|
|
240
|
+
_os.replace(tmp, config_path)
|
|
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")
|
|
@@ -147,6 +262,7 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
147
262
|
import sqlite3 as _sqlite3
|
|
148
263
|
|
|
149
264
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
265
|
+
profile_id = get_active_profile()
|
|
150
266
|
conn = _sqlite3.connect(db_path, timeout=10)
|
|
151
267
|
conn.row_factory = _sqlite3.Row
|
|
152
268
|
|
|
@@ -156,9 +272,9 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
156
272
|
"trigger_type, generation, status, mutation_summary, "
|
|
157
273
|
"blind_verified, created_at, completed_at "
|
|
158
274
|
"FROM skill_evolution_log "
|
|
159
|
-
"WHERE skill_name = ? OR parent_skill_id = ? "
|
|
275
|
+
"WHERE profile_id = ? AND (skill_name = ? OR parent_skill_id = ?) "
|
|
160
276
|
"ORDER BY created_at ASC",
|
|
161
|
-
(skill_name, skill_name),
|
|
277
|
+
(profile_id, skill_name, skill_name),
|
|
162
278
|
).fetchall()
|
|
163
279
|
else:
|
|
164
280
|
rows = conn.execute(
|
|
@@ -166,7 +282,9 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
166
282
|
"trigger_type, generation, status, mutation_summary, "
|
|
167
283
|
"blind_verified, created_at, completed_at "
|
|
168
284
|
"FROM skill_evolution_log "
|
|
285
|
+
"WHERE profile_id = ? "
|
|
169
286
|
"ORDER BY created_at DESC LIMIT 100",
|
|
287
|
+
(profile_id,),
|
|
170
288
|
).fetchall()
|
|
171
289
|
|
|
172
290
|
conn.close()
|
|
@@ -208,6 +326,6 @@ 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"}
|
|
@@ -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):
|
|
@@ -18,7 +18,7 @@ import threading
|
|
|
18
18
|
from datetime import datetime, timezone
|
|
19
19
|
|
|
20
20
|
from fastapi import APIRouter, HTTPException, Request
|
|
21
|
-
from pydantic import BaseModel
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
22
|
|
|
23
23
|
router = APIRouter(tags=["ingestion"])
|
|
24
24
|
|
|
@@ -28,7 +28,9 @@ _active_lock = threading.Lock()
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class IngestRequest(BaseModel):
|
|
31
|
-
content
|
|
31
|
+
# Cap ingest content (~1 MB) so a single call cannot exhaust memory through
|
|
32
|
+
# the embedding/tokenizer pipeline.
|
|
33
|
+
content: str = Field(..., max_length=1_000_000)
|
|
32
34
|
source_type: str
|
|
33
35
|
dedup_key: str
|
|
34
36
|
metadata: dict = {}
|
|
@@ -99,9 +101,10 @@ async def ingest(req: IngestRequest, request: Request):
|
|
|
99
101
|
# truth; retries repair this row without repeating the canonical write.
|
|
100
102
|
engine._db.execute(
|
|
101
103
|
"INSERT OR IGNORE INTO ingestion_log "
|
|
102
|
-
"(source_type, dedup_key, fact_ids, metadata, status, ingested_at) "
|
|
103
|
-
"VALUES (?, ?, ?, ?, 'ingested', ?)",
|
|
104
|
+
"(profile_id, source_type, dedup_key, fact_ids, metadata, status, ingested_at) "
|
|
105
|
+
"VALUES (?, ?, ?, ?, ?, 'ingested', ?)",
|
|
104
106
|
(
|
|
107
|
+
engine._profile_id,
|
|
105
108
|
req.source_type,
|
|
106
109
|
req.dedup_key,
|
|
107
110
|
json.dumps(fact_ids),
|
|
@@ -358,10 +358,10 @@ async def insight_action(
|
|
|
358
358
|
result = handler(conn, pid, limit=limit, days=days)
|
|
359
359
|
result["profile"] = pid
|
|
360
360
|
return result
|
|
361
|
-
except Exception
|
|
362
|
-
logger.
|
|
361
|
+
except Exception:
|
|
362
|
+
logger.exception("Insight action %s failed", action_name)
|
|
363
363
|
return JSONResponse(
|
|
364
|
-
{"error":
|
|
364
|
+
{"error": "Query failed"},
|
|
365
365
|
status_code=500,
|
|
366
366
|
)
|
|
367
367
|
finally:
|
|
@@ -291,8 +291,8 @@ async def learning_status():
|
|
|
291
291
|
result["source_scores"] = {}
|
|
292
292
|
|
|
293
293
|
except Exception as e:
|
|
294
|
-
logger.
|
|
295
|
-
result["error"] =
|
|
294
|
+
logger.exception("Error getting learning status")
|
|
295
|
+
result["error"] = "Internal server error"
|
|
296
296
|
|
|
297
297
|
return result
|
|
298
298
|
|
|
@@ -334,8 +334,8 @@ async def record_feedback(data: dict):
|
|
|
334
334
|
"feedback_id": row_id,
|
|
335
335
|
}
|
|
336
336
|
except Exception as e:
|
|
337
|
-
logger.
|
|
338
|
-
return {"success": False, "error":
|
|
337
|
+
logger.exception("Error recording feedback")
|
|
338
|
+
return {"success": False, "error": "Internal server error"}
|
|
339
339
|
|
|
340
340
|
|
|
341
341
|
@router.post("/api/feedback/dwell")
|
|
@@ -379,8 +379,8 @@ async def record_dwell(data: dict):
|
|
|
379
379
|
"feedback_id": row_id,
|
|
380
380
|
}
|
|
381
381
|
except Exception as e:
|
|
382
|
-
logger.
|
|
383
|
-
return {"success": False, "error":
|
|
382
|
+
logger.exception("Error recording dwell")
|
|
383
|
+
return {"success": False, "error": "Internal server error"}
|
|
384
384
|
|
|
385
385
|
|
|
386
386
|
@router.get("/api/feedback/stats")
|
|
@@ -414,8 +414,8 @@ async def feedback_stats():
|
|
|
414
414
|
"by_channel": by_channel, "by_type": by_type, "available": True,
|
|
415
415
|
}
|
|
416
416
|
except Exception as e:
|
|
417
|
-
logger.
|
|
418
|
-
return {"total_signals": 0, "ranking_phase": "baseline", "progress": 0, "error":
|
|
417
|
+
logger.exception("Error getting feedback stats")
|
|
418
|
+
return {"total_signals": 0, "ranking_phase": "baseline", "progress": 0, "error": "Internal server error"}
|
|
419
419
|
|
|
420
420
|
|
|
421
421
|
# ============================================================================
|
|
@@ -455,7 +455,7 @@ async def delete_pattern(data: dict) -> dict:
|
|
|
455
455
|
}
|
|
456
456
|
except Exception as exc: # noqa: BLE001
|
|
457
457
|
logger.error("delete_pattern failed: %s", exc)
|
|
458
|
-
return {"success": False, "error":
|
|
458
|
+
return {"success": False, "error": "internal error"}
|
|
459
459
|
|
|
460
460
|
|
|
461
461
|
@router.get("/api/patterns")
|
|
@@ -562,8 +562,8 @@ async def learning_backup():
|
|
|
562
562
|
"message": f"Learning DB backed up to {backup_name}",
|
|
563
563
|
}
|
|
564
564
|
except Exception as e:
|
|
565
|
-
logger.
|
|
566
|
-
return {"success": False, "error":
|
|
565
|
+
logger.exception("Error backing up learning DB")
|
|
566
|
+
return {"success": False, "error": "Internal server error"}
|
|
567
567
|
|
|
568
568
|
|
|
569
569
|
@router.post("/api/learning/reset")
|
|
@@ -583,7 +583,7 @@ async def learning_reset():
|
|
|
583
583
|
}
|
|
584
584
|
except Exception as exc: # noqa: BLE001
|
|
585
585
|
logger.error("learning_reset failed: %s", exc)
|
|
586
|
-
return {"success": False, "error":
|
|
586
|
+
return {"success": False, "error": "internal error"}
|
|
587
587
|
|
|
588
588
|
|
|
589
589
|
@router.post("/api/learning/retrain")
|
|
@@ -643,7 +643,7 @@ async def learning_retrain(data: dict | None = None):
|
|
|
643
643
|
}
|
|
644
644
|
except Exception as exc: # noqa: BLE001
|
|
645
645
|
logger.error("learning_retrain failed: %s", exc)
|
|
646
|
-
return {"success": False, "error":
|
|
646
|
+
return {"success": False, "error": "internal error"}
|
|
647
647
|
|
|
648
648
|
|
|
649
649
|
@router.post("/api/learning/migrate-legacy")
|
|
@@ -665,4 +665,4 @@ async def learning_migrate_legacy():
|
|
|
665
665
|
return {"success": True, **stats}
|
|
666
666
|
except Exception as exc: # noqa: BLE001
|
|
667
667
|
logger.error("learning_migrate_legacy failed: %s", exc)
|
|
668
|
-
return {"success": False, "error":
|
|
668
|
+
return {"success": False, "error": "internal error"}
|
|
@@ -11,9 +11,10 @@ import json
|
|
|
11
11
|
import logging
|
|
12
12
|
import sqlite3
|
|
13
13
|
|
|
14
|
-
from fastapi import APIRouter
|
|
14
|
+
from fastapi import APIRouter, Request
|
|
15
15
|
|
|
16
|
-
from .helpers import get_active_profile, MEMORY_DIR, DB_PATH
|
|
16
|
+
from .helpers import get_active_profile, get_engine_lazy, MEMORY_DIR, DB_PATH
|
|
17
|
+
from superlocalmemory.server.route_mutations import authorize_route_mutation
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger("superlocalmemory.routes.lifecycle")
|
|
19
20
|
router = APIRouter()
|
|
@@ -121,15 +122,65 @@ async def lifecycle_status():
|
|
|
121
122
|
"recent_transitions": [],
|
|
122
123
|
"age_stats": age_stats,
|
|
123
124
|
}
|
|
124
|
-
except Exception
|
|
125
|
-
logger.
|
|
126
|
-
return {"available": False, "error":
|
|
125
|
+
except Exception:
|
|
126
|
+
logger.exception("lifecycle_status error")
|
|
127
|
+
return {"available": False, "error": "Internal server error"}
|
|
127
128
|
|
|
128
129
|
|
|
129
130
|
@router.post("/api/lifecycle/compact")
|
|
130
|
-
async def trigger_compaction(data: dict = {}):
|
|
131
|
-
"""Trigger lifecycle compaction
|
|
131
|
+
async def trigger_compaction(request: Request, data: dict = {}):
|
|
132
|
+
"""Trigger lifecycle compaction for the active profile.
|
|
133
|
+
|
|
134
|
+
Body: ``{"dry_run": true|false}`` (default true — preview only).
|
|
135
|
+
|
|
136
|
+
Recomputes each fact's lifecycle zone (active→warm→cold→archive) from its
|
|
137
|
+
age/access pattern. dry_run returns the proposed transitions without
|
|
138
|
+
mutating; execute applies them and is profile-scoped + mutation-authorized.
|
|
139
|
+
"""
|
|
132
140
|
if not LIFECYCLE_AVAILABLE:
|
|
133
141
|
return {"success": False, "error": "Lifecycle engine not available"}
|
|
134
142
|
|
|
135
|
-
|
|
143
|
+
dry_run = bool((data or {}).get("dry_run", True))
|
|
144
|
+
try:
|
|
145
|
+
engine = get_engine_lazy(request.app.state)
|
|
146
|
+
if engine is None:
|
|
147
|
+
return {"success": False, "error": "Engine not initialized"}
|
|
148
|
+
profile = get_active_profile()
|
|
149
|
+
|
|
150
|
+
mgr = LifecycleManager(engine._db)
|
|
151
|
+
facts = engine._db.get_all_facts(profile)
|
|
152
|
+
candidates = []
|
|
153
|
+
for f in facts:
|
|
154
|
+
new_state = mgr.get_lifecycle_state(f)
|
|
155
|
+
if new_state != f.lifecycle:
|
|
156
|
+
candidates.append({
|
|
157
|
+
"fact_id": f.fact_id,
|
|
158
|
+
"current": getattr(f.lifecycle, "value", str(f.lifecycle)),
|
|
159
|
+
"proposed": getattr(new_state, "value", str(new_state)),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
applied = 0
|
|
163
|
+
if not dry_run and candidates:
|
|
164
|
+
authorization = authorize_route_mutation(
|
|
165
|
+
request,
|
|
166
|
+
operation="update",
|
|
167
|
+
source_agent_id="http-lifecycle-compact",
|
|
168
|
+
profile_id=profile,
|
|
169
|
+
)
|
|
170
|
+
for c in candidates:
|
|
171
|
+
engine._db.update_fact(c["fact_id"], {"lifecycle": c["proposed"]})
|
|
172
|
+
applied += 1
|
|
173
|
+
authorization.complete()
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"success": True,
|
|
177
|
+
"dry_run": dry_run,
|
|
178
|
+
"active_profile": profile,
|
|
179
|
+
"total_facts": len(facts),
|
|
180
|
+
"candidates": len(candidates),
|
|
181
|
+
"applied": applied,
|
|
182
|
+
"transitions": candidates[:20],
|
|
183
|
+
}
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
logger.exception("lifecycle compaction failed")
|
|
186
|
+
return {"success": False, "error": "internal error"}
|