superlocalmemory 3.8.0 → 3.8.2
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/CHANGELOG.md +112 -0
- package/README.md +32 -120
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +1 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +2 -2
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +3 -5
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +2 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +3 -5
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall.js +7 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +494 -9
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +72 -7
- package/src/superlocalmemory/cli/setup_wizard.py +142 -16
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/component_healer.py +144 -0
- package/src/superlocalmemory/core/component_registry.py +487 -0
- package/src/superlocalmemory/core/config.py +21 -0
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +132 -45
- package/src/superlocalmemory/core/engine.py +29 -22
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +154 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance.py +43 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
- package/src/superlocalmemory/core/recall_pipeline.py +42 -4
- package/src/superlocalmemory/core/store_pipeline.py +195 -20
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/portable_kit.py +34 -2
- 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/source_quality.py +523 -1
- package/src/superlocalmemory/loops/ledger.py +25 -5
- package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_active.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +21 -5
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/routes/backup.py +293 -70
- package/src/superlocalmemory/server/routes/behavioral.py +342 -61
- package/src/superlocalmemory/server/routes/brain.py +57 -16
- package/src/superlocalmemory/server/routes/config_api.py +84 -82
- package/src/superlocalmemory/server/routes/entity.py +100 -23
- package/src/superlocalmemory/server/routes/evolution.py +103 -100
- package/src/superlocalmemory/server/routes/learning.py +286 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/memories.py +8 -3
- package/src/superlocalmemory/server/routes/mesh.py +121 -32
- package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
- package/src/superlocalmemory/server/routes/stats.py +93 -155
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +184 -20
- package/src/superlocalmemory/server/unified_daemon.py +732 -41
- package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
- package/src/superlocalmemory/storage/migration_runner.py +79 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/schema.py +49 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/ui/index.html +6 -8
- package/src/superlocalmemory/ui/js/core.js +52 -9
- package/src/superlocalmemory/ui/js/dashboard.js +169 -82
- package/src/superlocalmemory/ui/js/od-backup.js +156 -65
- package/src/superlocalmemory/ui/js/od-brain.js +88 -51
- package/src/superlocalmemory/ui/js/od-components.js +147 -0
- package/src/superlocalmemory/ui/js/od-entities.js +65 -22
- package/src/superlocalmemory/ui/js/od-graph.js +46 -4
- package/src/superlocalmemory/ui/js/od-health.js +18 -0
- package/src/superlocalmemory/ui/js/od-memories.js +84 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-operations.js +36 -0
- package/src/superlocalmemory/ui/js/od-settings.js +186 -63
- package/src/superlocalmemory/ui/js/od-shell.js +249 -33
- package/src/superlocalmemory/ui/js/od-skills.js +44 -17
- package/src/superlocalmemory/ui/js/settings.js +15 -1
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-governance-advisor.md +0 -80
- package/plugin-src/agents/slm-loop-runner.md +0 -71
- package/plugin-src/agents/slm-memory-advisor.md +0 -49
- package/plugin-src/agents/slm-optimize-advisor.md +0 -44
- package/plugin-src/commands/slm-loop.md +0 -31
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -102
- package/plugin-src/manifest.json +0 -30
- 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 -62
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -25
- package/plugin-src/skills/slm-governance/SKILL.md +0 -248
- package/plugin-src/skills/slm-loop/SKILL.md +0 -99
- package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
- package/plugin-src/skills/slm-profile/SKILL.md +0 -148
- package/plugin-src/skills/slm-scope/SKILL.md +0 -176
|
@@ -8,36 +8,73 @@ 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
12
|
from typing import Optional
|
|
13
13
|
|
|
14
|
-
from fastapi import APIRouter
|
|
14
|
+
from fastapi import APIRouter, Request
|
|
15
15
|
from pydantic import BaseModel
|
|
16
16
|
|
|
17
|
-
from .
|
|
17
|
+
from superlocalmemory.server.config_file import read_config, update_config
|
|
18
|
+
|
|
19
|
+
from .helpers import MEMORY_DIR, get_active_profile
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger("superlocalmemory.routes.evolution")
|
|
20
22
|
router = APIRouter()
|
|
21
23
|
|
|
22
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
|
+
|
|
23
62
|
@router.get("/api/evolution/status")
|
|
24
|
-
|
|
63
|
+
def evolution_status(request: Request):
|
|
25
64
|
"""Get evolution engine status, backend, and recent history."""
|
|
65
|
+
_require_read(request)
|
|
26
66
|
try:
|
|
27
|
-
import json as _json
|
|
28
|
-
from superlocalmemory.evolution.skill_evolver import detect_backend
|
|
29
67
|
from superlocalmemory.evolution.evolution_store import EvolutionStore
|
|
68
|
+
from superlocalmemory.evolution.skill_evolver import detect_backend
|
|
30
69
|
|
|
31
|
-
|
|
32
|
-
config_path = MEMORY_DIR / "config.json"
|
|
33
|
-
evo_cfg = {}
|
|
34
|
-
if config_path.exists():
|
|
35
|
-
with open(config_path) as f:
|
|
36
|
-
cfg = _json.load(f)
|
|
37
|
-
evo_cfg = cfg.get("evolution", {})
|
|
38
|
-
|
|
70
|
+
evo_cfg = _read_evolution_config()
|
|
39
71
|
enabled = evo_cfg.get("enabled", False)
|
|
40
|
-
|
|
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
|
+
)
|
|
41
78
|
db_path = str(MEMORY_DIR / "memory.db")
|
|
42
79
|
|
|
43
80
|
profile_id = get_active_profile()
|
|
@@ -49,7 +86,7 @@ async def evolution_status():
|
|
|
49
86
|
"enabled": enabled,
|
|
50
87
|
"backend": backend,
|
|
51
88
|
"config": {
|
|
52
|
-
"backend_setting":
|
|
89
|
+
"backend_setting": backend_setting,
|
|
53
90
|
"max_per_cycle": evo_cfg.get("max_evolutions_per_cycle", 3),
|
|
54
91
|
"mutation_model": evo_cfg.get("mutation_model", ""),
|
|
55
92
|
"verify_model": evo_cfg.get("verify_model", ""),
|
|
@@ -82,53 +119,26 @@ async def evolution_status():
|
|
|
82
119
|
|
|
83
120
|
|
|
84
121
|
@router.post("/api/evolution/enable")
|
|
85
|
-
|
|
86
|
-
"""Enable
|
|
122
|
+
def evolution_enable(request: Request):
|
|
123
|
+
"""Enable evolution without replacing the user's selected backend."""
|
|
124
|
+
_require_manage(request)
|
|
87
125
|
try:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
with open(config_path) as f:
|
|
94
|
-
cfg = _json.load(f)
|
|
95
|
-
|
|
96
|
-
if "evolution" not in cfg:
|
|
97
|
-
cfg["evolution"] = {}
|
|
98
|
-
cfg["evolution"]["enabled"] = True
|
|
99
|
-
cfg["evolution"]["backend"] = "auto"
|
|
100
|
-
|
|
101
|
-
with open(config_path, "w") as f:
|
|
102
|
-
_json.dump(cfg, f, indent=2)
|
|
103
|
-
|
|
104
|
-
return {"ok": True, "message": "Evolution enabled. Will use auto-detected backend."}
|
|
126
|
+
evolution = _update_evolution_config(_enable_evolution)
|
|
127
|
+
return {
|
|
128
|
+
"ok": True,
|
|
129
|
+
"message": f"Evolution enabled with {evolution['backend']} backend.",
|
|
130
|
+
}
|
|
105
131
|
except Exception:
|
|
106
132
|
logger.exception("evolution_enable error")
|
|
107
133
|
return {"ok": False, "error": "Internal server error"}
|
|
108
134
|
|
|
109
135
|
|
|
110
136
|
@router.post("/api/evolution/disable")
|
|
111
|
-
|
|
137
|
+
def evolution_disable(request: Request):
|
|
112
138
|
"""Disable skill evolution engine. Mirrors /api/evolution/enable."""
|
|
139
|
+
_require_manage(request)
|
|
113
140
|
try:
|
|
114
|
-
|
|
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)
|
|
141
|
+
_update_evolution_config(lambda cfg: cfg.update({"enabled": False}))
|
|
132
142
|
|
|
133
143
|
return {"ok": True, "message": "Evolution disabled."}
|
|
134
144
|
except Exception:
|
|
@@ -137,18 +147,13 @@ async def evolution_disable():
|
|
|
137
147
|
|
|
138
148
|
|
|
139
149
|
@router.post("/api/evolution/run")
|
|
140
|
-
|
|
150
|
+
def evolution_run(request: Request):
|
|
141
151
|
"""Manually trigger an evolution cycle."""
|
|
152
|
+
_require_manage(request)
|
|
142
153
|
try:
|
|
143
|
-
import json as _json
|
|
144
154
|
from superlocalmemory.evolution.skill_evolver import SkillEvolver
|
|
145
155
|
|
|
146
|
-
|
|
147
|
-
evo_cfg = {}
|
|
148
|
-
if config_path.exists():
|
|
149
|
-
with open(config_path) as f:
|
|
150
|
-
evo_cfg = _json.load(f).get("evolution", {})
|
|
151
|
-
|
|
156
|
+
evo_cfg = _read_evolution_config()
|
|
152
157
|
if not evo_cfg.get("enabled", False):
|
|
153
158
|
return {"ok": False, "error": "Evolution is disabled. Enable first."}
|
|
154
159
|
|
|
@@ -159,17 +164,17 @@ async def evolution_run():
|
|
|
159
164
|
# per-step model fields (v3.7.9) or a dashboard-triggered run would
|
|
160
165
|
# silently ignore the user's configured models and fall back to the
|
|
161
166
|
# cheapest defaults.
|
|
162
|
-
|
|
163
|
-
enabled
|
|
164
|
-
backend
|
|
165
|
-
max_evolutions_per_cycle
|
|
166
|
-
mutation_model
|
|
167
|
-
verify_model
|
|
168
|
-
confirm_model
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
)
|
|
173
178
|
result = evolver.run_consolidation_cycle(profile)
|
|
174
179
|
|
|
175
180
|
return {"ok": True, **result}
|
|
@@ -188,16 +193,15 @@ class EvolutionConfigUpdate(BaseModel):
|
|
|
188
193
|
|
|
189
194
|
|
|
190
195
|
@router.post("/api/evolution/config")
|
|
191
|
-
|
|
196
|
+
def evolution_config(request: Request, body: EvolutionConfigUpdate):
|
|
192
197
|
"""Update evolution config from the dashboard (v3.7.9).
|
|
193
198
|
|
|
194
199
|
Validates model + backend values against the same allow-list the CLI uses
|
|
195
200
|
(``slm config set evolution.*``) and persists to config.json atomically.
|
|
196
201
|
Only fields provided in the body are changed.
|
|
197
202
|
"""
|
|
203
|
+
_require_manage(request)
|
|
198
204
|
try:
|
|
199
|
-
import json as _json
|
|
200
|
-
import os as _os
|
|
201
205
|
from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
|
|
202
206
|
|
|
203
207
|
accepted_models = set(_MODEL_ALIASES) | {"", "auto"}
|
|
@@ -217,27 +221,23 @@ async def evolution_config(body: EvolutionConfigUpdate):
|
|
|
217
221
|
and not 0 < body.max_evolutions_per_cycle <= 50):
|
|
218
222
|
return {"ok": False, "error": "max_evolutions_per_cycle must be 1..50"}
|
|
219
223
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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)
|
|
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
241
|
|
|
242
242
|
return {"ok": True, "config": {
|
|
243
243
|
"enabled": evo.get("enabled", False),
|
|
@@ -253,11 +253,13 @@ async def evolution_config(body: EvolutionConfigUpdate):
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
@router.get("/api/evolution/lineage")
|
|
256
|
-
|
|
256
|
+
def evolution_lineage(request: Request, skill_name: str = ""):
|
|
257
257
|
"""Get evolution lineage for a skill or all skills.
|
|
258
258
|
|
|
259
259
|
Returns lineage records and a tree structure grouped by root skill.
|
|
260
260
|
"""
|
|
261
|
+
_require_read(request)
|
|
262
|
+
conn = None
|
|
261
263
|
try:
|
|
262
264
|
import sqlite3 as _sqlite3
|
|
263
265
|
|
|
@@ -287,8 +289,6 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
287
289
|
(profile_id,),
|
|
288
290
|
).fetchall()
|
|
289
291
|
|
|
290
|
-
conn.close()
|
|
291
|
-
|
|
292
292
|
lineage = [
|
|
293
293
|
{
|
|
294
294
|
"id": dict(r)["id"],
|
|
@@ -329,3 +329,6 @@ async def evolution_lineage(skill_name: str = ""):
|
|
|
329
329
|
except Exception:
|
|
330
330
|
logger.exception("evolution_lineage error")
|
|
331
331
|
return {"lineage": [], "lineage_count": 0, "tree": {}, "error": "Internal server error"}
|
|
332
|
+
finally:
|
|
333
|
+
if conn is not None:
|
|
334
|
+
conn.close()
|