superlocalmemory 3.4.9 → 3.4.10
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/README.md +14 -0
- package/docs/cloud-backup.md +174 -0
- package/docs/skill-evolution.md +189 -0
- package/ide/hooks/tool-event-hook.sh +101 -11
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +189 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +81 -29
- package/src/superlocalmemory/cli/main.py +11 -0
- package/src/superlocalmemory/core/consolidation_engine.py +10 -0
- package/src/superlocalmemory/core/engine.py +7 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +24 -3
- package/src/superlocalmemory/encoding/entity_resolver.py +95 -28
- package/src/superlocalmemory/infra/backup.py +63 -20
- package/src/superlocalmemory/infra/cloud_backup.py +703 -0
- package/src/superlocalmemory/learning/skill_performance_miner.py +389 -0
- package/src/superlocalmemory/server/routes/backup.py +512 -8
- package/src/superlocalmemory/server/routes/behavioral.py +23 -5
- package/src/superlocalmemory/storage/schema_v3410.py +159 -0
- package/src/superlocalmemory/ui/index.html +55 -2
- package/src/superlocalmemory/ui/js/core.js +3 -0
- package/src/superlocalmemory/ui/js/ng-entities.js +27 -3
- package/src/superlocalmemory/ui/js/ng-shell.js +33 -0
- package/src/superlocalmemory/ui/js/ng-skills.js +227 -0
- package/src/superlocalmemory/ui/js/settings.js +311 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -594
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -317
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -55
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -58,6 +58,8 @@ def dispatch(args: Namespace) -> None:
|
|
|
58
58
|
"reap": cmd_reap,
|
|
59
59
|
# V3.3.21 daemon
|
|
60
60
|
"serve": cmd_serve,
|
|
61
|
+
# V3.4.9 nuclear restart
|
|
62
|
+
"restart": cmd_restart,
|
|
61
63
|
# V3.4.3 ingestion adapters
|
|
62
64
|
"adapters": cmd_adapters,
|
|
63
65
|
# V3.4.8 external observation ingestion
|
|
@@ -146,6 +148,193 @@ def cmd_serve(args: Namespace) -> None:
|
|
|
146
148
|
# -- Ingestion Adapters (V3.4.3) ------------------------------------------
|
|
147
149
|
|
|
148
150
|
|
|
151
|
+
def cmd_restart(args: Namespace) -> None:
|
|
152
|
+
"""Nuclear restart: kill ALL orphans, clean state, start fresh, verify health.
|
|
153
|
+
|
|
154
|
+
5-step pipeline:
|
|
155
|
+
1. Kill ALL SLM processes (daemon + workers + orphans)
|
|
156
|
+
2. Clean stale PID/port/lock files
|
|
157
|
+
3. Start fresh daemon
|
|
158
|
+
4. Wait for engine warmup + verify health
|
|
159
|
+
5. Optionally open dashboard
|
|
160
|
+
"""
|
|
161
|
+
import os
|
|
162
|
+
import time
|
|
163
|
+
from pathlib import Path
|
|
164
|
+
|
|
165
|
+
use_json = getattr(args, "json", False)
|
|
166
|
+
open_dashboard = getattr(args, "dashboard", False)
|
|
167
|
+
slm_dir = Path.home() / ".superlocalmemory"
|
|
168
|
+
steps: list[dict] = []
|
|
169
|
+
|
|
170
|
+
def _log(step: int, name: str, status: str, detail: str = ""):
|
|
171
|
+
entry = {"step": step, "name": name, "status": status, "detail": detail}
|
|
172
|
+
steps.append(entry)
|
|
173
|
+
if not use_json:
|
|
174
|
+
icon = {"ok": "+", "warn": "!", "fail": "x"}.get(status, " ")
|
|
175
|
+
print(f" [{icon}] Step {step}: {name}" + (f" — {detail}" if detail else ""))
|
|
176
|
+
|
|
177
|
+
if not use_json:
|
|
178
|
+
print()
|
|
179
|
+
print(" SLM Full System Restart")
|
|
180
|
+
print(" " + "=" * 40)
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
# Step 1: Kill ALL SLM processes
|
|
184
|
+
killed = 0
|
|
185
|
+
try:
|
|
186
|
+
import psutil
|
|
187
|
+
my_pid = os.getpid()
|
|
188
|
+
targets = [
|
|
189
|
+
"superlocalmemory.server.unified_daemon",
|
|
190
|
+
"superlocalmemory.core.embedding_worker",
|
|
191
|
+
"superlocalmemory.core.recall_worker",
|
|
192
|
+
"superlocalmemory.core.reranker_worker",
|
|
193
|
+
"superlocalmemory.cli.daemon",
|
|
194
|
+
]
|
|
195
|
+
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
196
|
+
try:
|
|
197
|
+
if proc.pid == my_pid:
|
|
198
|
+
continue
|
|
199
|
+
cmdline = " ".join(proc.info.get("cmdline") or [])
|
|
200
|
+
if any(t in cmdline for t in targets):
|
|
201
|
+
for child in proc.children(recursive=True):
|
|
202
|
+
try:
|
|
203
|
+
child.kill()
|
|
204
|
+
killed += 1
|
|
205
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
206
|
+
pass
|
|
207
|
+
proc.kill()
|
|
208
|
+
killed += 1
|
|
209
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
210
|
+
pass
|
|
211
|
+
except ImportError:
|
|
212
|
+
# Fallback: pkill
|
|
213
|
+
import subprocess as _sp
|
|
214
|
+
for pattern in [
|
|
215
|
+
"superlocalmemory.server.unified_daemon",
|
|
216
|
+
"superlocalmemory.core.embedding_worker",
|
|
217
|
+
"superlocalmemory.core.recall_worker",
|
|
218
|
+
"superlocalmemory.core.reranker_worker",
|
|
219
|
+
]:
|
|
220
|
+
try:
|
|
221
|
+
r = _sp.run(["pkill", "-9", "-f", pattern], capture_output=True, timeout=5)
|
|
222
|
+
if r.returncode == 0:
|
|
223
|
+
killed += 1
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
_log(1, "Kill all SLM processes", "ok", f"{killed} processes killed")
|
|
228
|
+
time.sleep(3)
|
|
229
|
+
|
|
230
|
+
# Step 2: Clean stale files
|
|
231
|
+
cleaned = []
|
|
232
|
+
for fname in ("daemon.pid", "daemon.port", "daemon.lock"):
|
|
233
|
+
fpath = slm_dir / fname
|
|
234
|
+
if fpath.exists():
|
|
235
|
+
fpath.unlink(missing_ok=True)
|
|
236
|
+
cleaned.append(fname)
|
|
237
|
+
_log(2, "Clean stale state files", "ok",
|
|
238
|
+
f"removed: {', '.join(cleaned)}" if cleaned else "already clean")
|
|
239
|
+
|
|
240
|
+
# Step 3: Start fresh daemon
|
|
241
|
+
time.sleep(1)
|
|
242
|
+
from superlocalmemory.cli.daemon import ensure_daemon
|
|
243
|
+
started = ensure_daemon()
|
|
244
|
+
_log(3, "Start fresh daemon", "ok" if started else "fail",
|
|
245
|
+
"daemon started" if started else "failed to start — check slm doctor")
|
|
246
|
+
|
|
247
|
+
if not started:
|
|
248
|
+
if use_json:
|
|
249
|
+
from superlocalmemory.cli.json_output import json_print
|
|
250
|
+
json_print("restart", data={"steps": steps, "success": False},
|
|
251
|
+
next_actions=[{"command": "slm doctor", "description": "Diagnose issues"}])
|
|
252
|
+
else:
|
|
253
|
+
print("\n Restart FAILED at step 3. Run: slm doctor")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# Step 4: Wait for warmup + verify health
|
|
257
|
+
if not use_json:
|
|
258
|
+
print(" [ ] Step 4: Waiting for engine warmup (up to 30s)...", end="", flush=True)
|
|
259
|
+
|
|
260
|
+
health = None
|
|
261
|
+
engine_ok = False
|
|
262
|
+
for attempt in range(15):
|
|
263
|
+
time.sleep(2)
|
|
264
|
+
try:
|
|
265
|
+
from superlocalmemory.cli.daemon import daemon_request
|
|
266
|
+
health = daemon_request("GET", "/health")
|
|
267
|
+
if health and health.get("engine") == "initialized":
|
|
268
|
+
engine_ok = True
|
|
269
|
+
break
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
if not use_json:
|
|
274
|
+
print("\r", end="") # Clear the waiting line
|
|
275
|
+
|
|
276
|
+
if engine_ok:
|
|
277
|
+
version = health.get("version", "?")
|
|
278
|
+
pid = health.get("pid", "?")
|
|
279
|
+
_log(4, "Engine health verified", "ok", f"v{version}, PID {pid}, engine=initialized")
|
|
280
|
+
else:
|
|
281
|
+
engine_state = health.get("engine", "unknown") if health else "unreachable"
|
|
282
|
+
_log(4, "Engine health check", "warn",
|
|
283
|
+
f"engine={engine_state} — may still be warming up. Try again in 30s.")
|
|
284
|
+
|
|
285
|
+
# Step 5: Database integrity check
|
|
286
|
+
try:
|
|
287
|
+
import sqlite3
|
|
288
|
+
db_path = slm_dir / "memory.db"
|
|
289
|
+
if db_path.exists():
|
|
290
|
+
conn = sqlite3.connect(str(db_path))
|
|
291
|
+
integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
|
|
292
|
+
fact_count = conn.execute("SELECT COUNT(*) FROM atomic_facts").fetchone()[0]
|
|
293
|
+
entity_count = conn.execute("SELECT COUNT(*) FROM canonical_entities").fetchone()[0]
|
|
294
|
+
conn.close()
|
|
295
|
+
_log(5, "Database integrity", "ok" if integrity == "ok" else "fail",
|
|
296
|
+
f"integrity={integrity}, {fact_count} facts, {entity_count} entities")
|
|
297
|
+
else:
|
|
298
|
+
_log(5, "Database check", "warn", "no database yet — will create on first use")
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
_log(5, "Database check", "warn", str(exc))
|
|
301
|
+
|
|
302
|
+
# Step 6 (optional): Open dashboard
|
|
303
|
+
if open_dashboard:
|
|
304
|
+
try:
|
|
305
|
+
import webbrowser
|
|
306
|
+
from superlocalmemory.cli.daemon import _get_port
|
|
307
|
+
port = _get_port()
|
|
308
|
+
url = f"http://localhost:{port}"
|
|
309
|
+
webbrowser.open(url)
|
|
310
|
+
_log(6, "Dashboard opened", "ok", url)
|
|
311
|
+
except Exception as exc:
|
|
312
|
+
_log(6, "Dashboard open", "fail", str(exc))
|
|
313
|
+
|
|
314
|
+
# Summary
|
|
315
|
+
all_ok = all(s["status"] == "ok" for s in steps)
|
|
316
|
+
|
|
317
|
+
if use_json:
|
|
318
|
+
from superlocalmemory.cli.json_output import json_print
|
|
319
|
+
json_print("restart", data={
|
|
320
|
+
"steps": steps, "success": all_ok,
|
|
321
|
+
"processes_killed": killed,
|
|
322
|
+
"version": health.get("version") if health else None,
|
|
323
|
+
}, next_actions=[
|
|
324
|
+
{"command": "slm serve status", "description": "Check daemon status"},
|
|
325
|
+
{"command": "slm dashboard", "description": "Open dashboard"},
|
|
326
|
+
])
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
print()
|
|
330
|
+
if all_ok:
|
|
331
|
+
print(" All systems operational.")
|
|
332
|
+
else:
|
|
333
|
+
warnings = [s for s in steps if s["status"] != "ok"]
|
|
334
|
+
print(f" {len(warnings)} issue(s) — check details above.")
|
|
335
|
+
print()
|
|
336
|
+
|
|
337
|
+
|
|
149
338
|
def cmd_ingest(args: Namespace) -> None:
|
|
150
339
|
"""Import external observations into SLM learning pipeline."""
|
|
151
340
|
from superlocalmemory.cli.ingest_cmd import cmd_ingest as _ingest
|
|
@@ -76,28 +76,43 @@ def _error(msg: str, use_json: bool) -> None:
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def _ingest_ecc(file_path: str, *, dry_run: bool = False) -> dict:
|
|
79
|
-
"""Ingest ECC
|
|
79
|
+
"""Ingest ECC observations into SLM tool_events.
|
|
80
80
|
|
|
81
|
-
Scans
|
|
82
|
-
|
|
81
|
+
Scans TWO sources:
|
|
82
|
+
1. ECC observation files (~/.claude/homunculus/projects/*/observations.jsonl)
|
|
83
|
+
— rich data: tool input/output, session_id, project context
|
|
84
|
+
2. Claude session transcript files (~/.claude/projects/*.jsonl)
|
|
85
|
+
— fallback: tool_use blocks from conversation history
|
|
86
|
+
|
|
87
|
+
v3.4.10: Now preserves input_summary and output_summary from ECC observations.
|
|
83
88
|
"""
|
|
84
89
|
result = {"source": "ecc", "ingested": 0, "skipped": 0, "dry_run": dry_run}
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
files: list[Path] = []
|
|
92
|
+
|
|
87
93
|
if file_path:
|
|
88
94
|
files = [Path(file_path)]
|
|
89
95
|
else:
|
|
90
|
-
#
|
|
96
|
+
# Source 1: ECC observation files (RICH data — preferred)
|
|
97
|
+
ecc_dir = Path.home() / ".claude" / "homunculus" / "projects"
|
|
98
|
+
if ecc_dir.exists():
|
|
99
|
+
ecc_files = sorted(
|
|
100
|
+
ecc_dir.rglob("observations.jsonl"),
|
|
101
|
+
key=lambda p: p.stat().st_mtime, reverse=True,
|
|
102
|
+
)
|
|
103
|
+
files.extend(ecc_files[:20])
|
|
104
|
+
|
|
105
|
+
# Source 2: Claude session transcripts (fallback)
|
|
91
106
|
claude_dir = Path.home() / ".claude" / "projects"
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
if claude_dir.exists():
|
|
108
|
+
claude_files = sorted(
|
|
109
|
+
claude_dir.rglob("*.jsonl"),
|
|
110
|
+
key=lambda p: p.stat().st_mtime, reverse=True,
|
|
111
|
+
)
|
|
112
|
+
files.extend(claude_files[:20])
|
|
98
113
|
|
|
99
114
|
if not files:
|
|
100
|
-
result["error"] = "No session files found"
|
|
115
|
+
result["error"] = "No ECC observation or session files found"
|
|
101
116
|
return result
|
|
102
117
|
|
|
103
118
|
result["files_scanned"] = len(files)
|
|
@@ -116,7 +131,6 @@ def _ingest_ecc(file_path: str, *, dry_run: bool = False) -> dict:
|
|
|
116
131
|
result["skipped"] += 1
|
|
117
132
|
continue
|
|
118
133
|
|
|
119
|
-
# Extract tool usage from ECC session records
|
|
120
134
|
extracted = _extract_tool_events_from_record(record)
|
|
121
135
|
events.extend(extracted)
|
|
122
136
|
except (OSError, PermissionError):
|
|
@@ -128,7 +142,6 @@ def _ingest_ecc(file_path: str, *, dry_run: bool = False) -> dict:
|
|
|
128
142
|
result["sample"] = events[:5]
|
|
129
143
|
return result
|
|
130
144
|
|
|
131
|
-
# Write to tool_events table
|
|
132
145
|
if events:
|
|
133
146
|
ingested = _write_tool_events(events)
|
|
134
147
|
result["ingested"] = ingested
|
|
@@ -139,47 +152,79 @@ def _ingest_ecc(file_path: str, *, dry_run: bool = False) -> dict:
|
|
|
139
152
|
|
|
140
153
|
|
|
141
154
|
def _extract_tool_events_from_record(record: dict) -> list[dict]:
|
|
142
|
-
"""Extract tool events from a single ECC/Claude session JSONL record.
|
|
155
|
+
"""Extract tool events from a single ECC/Claude session JSONL record.
|
|
156
|
+
|
|
157
|
+
Handles three formats:
|
|
158
|
+
1. ECC observation format: {"event": "tool_complete", "tool": "X", "input": "...", "output": "..."}
|
|
159
|
+
2. Claude transcript format: {"type": "assistant", "content": [{"type": "tool_use", ...}]}
|
|
160
|
+
3. Direct tool event format: {"tool_name": "X", "event_type": "complete"}
|
|
161
|
+
|
|
162
|
+
v3.4.10: Preserves input_summary and output_summary from all formats.
|
|
163
|
+
"""
|
|
143
164
|
events = []
|
|
165
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
166
|
+
|
|
167
|
+
# Format 1: ECC observation format (from ~/.claude/homunculus/projects/*/observations.jsonl)
|
|
168
|
+
if "event" in record and "tool" in record:
|
|
169
|
+
event_type_raw = record.get("event", "")
|
|
170
|
+
if event_type_raw in ("tool_complete", "tool_start"):
|
|
171
|
+
event_type = "complete" if event_type_raw == "tool_complete" else "invoke"
|
|
172
|
+
events.append({
|
|
173
|
+
"tool_name": record["tool"],
|
|
174
|
+
"event_type": event_type,
|
|
175
|
+
"input_summary": str(record.get("input", ""))[:500],
|
|
176
|
+
"output_summary": str(record.get("output", ""))[:500],
|
|
177
|
+
"session_id": record.get("session", "ecc_import"),
|
|
178
|
+
"project_path": record.get("project_name", ""),
|
|
179
|
+
"created_at": record.get("timestamp", now_iso),
|
|
180
|
+
})
|
|
181
|
+
return events
|
|
144
182
|
|
|
145
|
-
#
|
|
183
|
+
# Format 2: Claude transcript format
|
|
146
184
|
if "type" in record:
|
|
147
185
|
rtype = record.get("type", "")
|
|
148
|
-
|
|
149
|
-
# Tool use records
|
|
150
186
|
if rtype == "assistant" and "content" in record:
|
|
151
187
|
content = record.get("content", [])
|
|
152
188
|
if isinstance(content, list):
|
|
153
189
|
for block in content:
|
|
154
190
|
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
155
191
|
tool_name = block.get("name", "unknown")
|
|
192
|
+
raw_input = block.get("input", {})
|
|
193
|
+
input_str = json.dumps(raw_input, default=str)[:500] if isinstance(raw_input, dict) else str(raw_input)[:500]
|
|
156
194
|
events.append({
|
|
157
195
|
"tool_name": tool_name,
|
|
158
196
|
"event_type": "complete",
|
|
197
|
+
"input_summary": input_str,
|
|
198
|
+
"output_summary": "",
|
|
159
199
|
"session_id": record.get("session_id", "ecc_import"),
|
|
160
|
-
"
|
|
200
|
+
"project_path": "",
|
|
201
|
+
"created_at": record.get("timestamp", now_iso),
|
|
161
202
|
})
|
|
162
|
-
|
|
163
|
-
# Also extract from tool_use type directly
|
|
164
|
-
if isinstance(content, list):
|
|
165
|
-
for block in content:
|
|
166
|
-
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
203
|
+
elif isinstance(block, dict) and block.get("type") == "tool_result":
|
|
167
204
|
tool_name = block.get("tool_use_id", "unknown")
|
|
168
205
|
is_error = block.get("is_error", False)
|
|
206
|
+
raw_content = block.get("content", "")
|
|
207
|
+
output_str = str(raw_content)[:500] if raw_content else ""
|
|
169
208
|
events.append({
|
|
170
209
|
"tool_name": tool_name,
|
|
171
210
|
"event_type": "error" if is_error else "complete",
|
|
211
|
+
"input_summary": "",
|
|
212
|
+
"output_summary": output_str,
|
|
172
213
|
"session_id": record.get("session_id", "ecc_import"),
|
|
173
|
-
"
|
|
214
|
+
"project_path": "",
|
|
215
|
+
"created_at": record.get("timestamp", now_iso),
|
|
174
216
|
})
|
|
175
217
|
|
|
176
|
-
#
|
|
218
|
+
# Format 3: Direct tool event format (from hook output)
|
|
177
219
|
if "tool_name" in record and "event_type" in record:
|
|
178
220
|
events.append({
|
|
179
221
|
"tool_name": record["tool_name"],
|
|
180
222
|
"event_type": record.get("event_type", "complete"),
|
|
223
|
+
"input_summary": str(record.get("input_summary", ""))[:500],
|
|
224
|
+
"output_summary": str(record.get("output_summary", ""))[:500],
|
|
181
225
|
"session_id": record.get("session_id", "ecc_import"),
|
|
182
|
-
"
|
|
226
|
+
"project_path": record.get("project_path", ""),
|
|
227
|
+
"created_at": record.get("created_at", now_iso),
|
|
183
228
|
})
|
|
184
229
|
|
|
185
230
|
return events
|
|
@@ -228,7 +273,11 @@ def _ingest_jsonl(file_path: str, *, dry_run: bool = False) -> dict:
|
|
|
228
273
|
|
|
229
274
|
|
|
230
275
|
def _write_tool_events(events: list[dict]) -> int:
|
|
231
|
-
"""Write tool events to SLM's memory.db tool_events table.
|
|
276
|
+
"""Write tool events to SLM's memory.db tool_events table.
|
|
277
|
+
|
|
278
|
+
v3.4.10: Preserves input_summary, output_summary, and project_path
|
|
279
|
+
from enriched sources (ECC observations, enriched hook).
|
|
280
|
+
"""
|
|
232
281
|
db_path = MEMORY_DB
|
|
233
282
|
if not db_path.exists():
|
|
234
283
|
return 0
|
|
@@ -243,11 +292,14 @@ def _write_tool_events(events: list[dict]) -> int:
|
|
|
243
292
|
"INSERT INTO tool_events "
|
|
244
293
|
"(session_id, profile_id, project_path, tool_name, event_type, "
|
|
245
294
|
" input_summary, output_summary, duration_ms, metadata, created_at) "
|
|
246
|
-
"VALUES (?, 'default',
|
|
295
|
+
"VALUES (?, 'default', ?, ?, ?, ?, ?, 0, '{}', ?)",
|
|
247
296
|
(
|
|
248
297
|
ev.get("session_id", "import"),
|
|
298
|
+
ev.get("project_path", ""),
|
|
249
299
|
ev["tool_name"],
|
|
250
300
|
ev.get("event_type", "complete"),
|
|
301
|
+
ev.get("input_summary", ""),
|
|
302
|
+
ev.get("output_summary", ""),
|
|
251
303
|
ev.get("created_at", datetime.now(timezone.utc).isoformat()),
|
|
252
304
|
),
|
|
253
305
|
)
|
|
@@ -199,6 +199,17 @@ def main() -> None:
|
|
|
199
199
|
help="start (default), stop, status, install (OS service), uninstall",
|
|
200
200
|
)
|
|
201
201
|
|
|
202
|
+
# V3.4.9: Full system restart with health verification
|
|
203
|
+
restart_p = sub.add_parser(
|
|
204
|
+
"restart",
|
|
205
|
+
help="Nuclear restart: kill orphans, clean state, start fresh, verify health",
|
|
206
|
+
)
|
|
207
|
+
restart_p.add_argument(
|
|
208
|
+
"--dashboard", action="store_true",
|
|
209
|
+
help="Open dashboard after restart",
|
|
210
|
+
)
|
|
211
|
+
restart_p.add_argument("--json", action="store_true", help="Output structured JSON")
|
|
212
|
+
|
|
202
213
|
# -- Profiles ------------------------------------------------------
|
|
203
214
|
profile_p = sub.add_parser("profile", help="Profile management (list/switch/create)")
|
|
204
215
|
profile_p.add_argument(
|
|
@@ -178,6 +178,16 @@ class ConsolidationEngine:
|
|
|
178
178
|
logger.debug("Soft prompt generation (non-fatal): %s", exc)
|
|
179
179
|
results["soft_prompts"] = {"error": str(exc)}
|
|
180
180
|
|
|
181
|
+
# Step 10 (v3.4.10): Mine skill performance from tool events.
|
|
182
|
+
# Creates per-skill assertions and skill correlation patterns.
|
|
183
|
+
try:
|
|
184
|
+
from superlocalmemory.learning.skill_performance_miner import SkillPerformanceMiner
|
|
185
|
+
spm = SkillPerformanceMiner(self._db.db_path)
|
|
186
|
+
results["skill_performance"] = spm.mine(profile_id)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
logger.debug("Skill performance mining (non-fatal): %s", exc)
|
|
189
|
+
results["skill_performance"] = {"error": str(exc)}
|
|
190
|
+
|
|
181
191
|
results["success"] = True
|
|
182
192
|
except Exception as exc:
|
|
183
193
|
logger.warning(
|
|
@@ -139,6 +139,13 @@ class MemoryEngine:
|
|
|
139
139
|
except Exception as exc:
|
|
140
140
|
logger.debug("V3.4.7 schema migration: %s", exc)
|
|
141
141
|
|
|
142
|
+
# V3.4.10: Apply "Fortress" schema (backup_destinations, entity_blacklist)
|
|
143
|
+
try:
|
|
144
|
+
from superlocalmemory.storage.schema_v3410 import apply_v3410_schema
|
|
145
|
+
apply_v3410_schema(str(self._db.db_path))
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
logger.debug("V3.4.10 schema migration: %s", exc)
|
|
148
|
+
|
|
142
149
|
self._embedder = init_embedder(self._config)
|
|
143
150
|
|
|
144
151
|
if self._caps.llm_fact_extraction:
|
|
@@ -75,7 +75,7 @@ class MaintenanceScheduler:
|
|
|
75
75
|
self._timer.start()
|
|
76
76
|
|
|
77
77
|
def _run(self) -> None:
|
|
78
|
-
"""Execute maintenance
|
|
78
|
+
"""Execute maintenance + auto-backup check, then schedule next run."""
|
|
79
79
|
if not self._running:
|
|
80
80
|
return
|
|
81
81
|
try:
|
|
@@ -84,8 +84,29 @@ class MaintenanceScheduler:
|
|
|
84
84
|
logger.info("Scheduled maintenance complete: %s", counts)
|
|
85
85
|
except Exception as exc:
|
|
86
86
|
logger.warning("Scheduled maintenance failed: %s", exc)
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
|
|
88
|
+
# V3.4.10: Check if auto-backup is due
|
|
89
|
+
try:
|
|
90
|
+
from superlocalmemory.infra.backup import BackupManager
|
|
91
|
+
manager = BackupManager(db_path=self._db.db_path)
|
|
92
|
+
filename = manager.check_and_backup()
|
|
93
|
+
if filename:
|
|
94
|
+
logger.info("Auto-backup created: %s", filename)
|
|
95
|
+
self._sync_cloud_destinations(manager)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.debug("Auto-backup check skipped: %s", exc)
|
|
98
|
+
|
|
99
|
+
self._schedule_next()
|
|
100
|
+
|
|
101
|
+
def _sync_cloud_destinations(self, manager: object) -> None:
|
|
102
|
+
"""Push latest backup to configured cloud destinations."""
|
|
103
|
+
try:
|
|
104
|
+
from superlocalmemory.infra.cloud_backup import sync_all_destinations
|
|
105
|
+
sync_all_destinations(self._db.db_path)
|
|
106
|
+
except ImportError:
|
|
107
|
+
pass # cloud_backup module not available yet
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.warning("Cloud sync failed (non-critical): %s", exc)
|
|
89
110
|
|
|
90
111
|
def __del__(self) -> None:
|
|
91
112
|
try:
|