superlocalmemory 3.4.24 → 3.4.30
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 +43 -0
- package/README.md +8 -1
- package/package.json +1 -1
- package/pyproject.toml +3 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +3 -0
- package/src/superlocalmemory/cli/daemon.py +90 -16
- package/src/superlocalmemory/cli/doctor_cmd.py +152 -0
- package/src/superlocalmemory/cli/main.py +28 -0
- package/src/superlocalmemory/cli/post_install.py +15 -0
- package/src/superlocalmemory/cli/setup_wizard.py +20 -0
- package/src/superlocalmemory/cli/version_banner.py +183 -0
- package/src/superlocalmemory/cli/wizard_v3426_options.py +129 -0
- package/src/superlocalmemory/core/clock_monitor.py +45 -0
- package/src/superlocalmemory/core/db_pool.py +80 -0
- package/src/superlocalmemory/core/engine.py +75 -30
- package/src/superlocalmemory/core/engine_capabilities.py +24 -0
- package/src/superlocalmemory/core/engine_lock.py +75 -0
- package/src/superlocalmemory/core/error_catalog.py +113 -0
- package/src/superlocalmemory/core/error_envelope.py +60 -0
- package/src/superlocalmemory/core/file_lock.py +92 -0
- package/src/superlocalmemory/core/loop_watchdog.py +56 -0
- package/src/superlocalmemory/core/priority_queue.py +61 -0
- package/src/superlocalmemory/core/queue_dispatcher.py +73 -0
- package/src/superlocalmemory/core/rate_limit.py +151 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_queue.py +370 -0
- package/src/superlocalmemory/core/recall_worker.py +10 -0
- package/src/superlocalmemory/core/safe_fs.py +108 -0
- package/src/superlocalmemory/hooks/auto_capture.py +34 -12
- package/src/superlocalmemory/hooks/auto_recall.py +36 -9
- package/src/superlocalmemory/mcp/_daemon_proxy.py +107 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +121 -0
- package/src/superlocalmemory/mcp/resources.py +8 -5
- package/src/superlocalmemory/mcp/server.py +38 -9
- package/src/superlocalmemory/mcp/tools_active.py +21 -9
- package/src/superlocalmemory/mcp/tools_core.py +13 -9
- package/src/superlocalmemory/mcp/tools_evolution.py +4 -2
- package/src/superlocalmemory/mcp/tools_learning.py +5 -3
- package/src/superlocalmemory/mcp/tools_mesh.py +5 -3
- package/src/superlocalmemory/mcp/tools_v3.py +18 -22
- package/src/superlocalmemory/mcp/tools_v33.py +65 -2
- package/src/superlocalmemory/migrations/__init__.py +5 -0
- package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +144 -0
- package/src/superlocalmemory/server/routes/events.py +1 -1
- package/src/superlocalmemory/server/routes/v3_api.py +0 -1
- package/src/superlocalmemory/server/unified_daemon.py +128 -12
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [3.4.30] - 2026-04-24
|
|
14
|
+
|
|
15
|
+
Multi-IDE shared worker, silent migration, and security hardening.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Multi-IDE RAM sharing.** MCP processes share a single recall worker
|
|
19
|
+
via the daemon. Total RSS stays below 2 GB with four IDEs open.
|
|
20
|
+
- **Feedback and learning signals** flow from every IDE session to the
|
|
21
|
+
daemon, not just the first.
|
|
22
|
+
- **Setup wizard** validates the data directory at install time and
|
|
23
|
+
rejects iCloud, Dropbox, OneDrive, Box, Google Drive, and
|
|
24
|
+
`Library/CloudStorage` paths that silently corrupt SQLite WAL.
|
|
25
|
+
- **One-time upgrade banner** after `pip install -U` / `npm install -g`
|
|
26
|
+
points users to `slm doctor`.
|
|
27
|
+
- **`docs/errors.md`** — canonical error catalog with codes, recovery
|
|
28
|
+
steps, exit codes, and HTTP status mappings.
|
|
29
|
+
- **CI matrix** now runs on `ubuntu-22.04`, `macos-14` (Apple Silicon),
|
|
30
|
+
and `windows-latest` with `portalocker`.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Silent, atomic data migration** on upgrade — no manual steps.
|
|
34
|
+
- **Migration serialized via file lock** so parallel pip + npm installs
|
|
35
|
+
cannot race.
|
|
36
|
+
- **Concurrent-safe MCP engine singleton** with double-checked locking.
|
|
37
|
+
- Pool adapter returns frozen dataclasses instead of `SimpleNamespace`.
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
- File permissions tightened: marker files written at 0600, parent
|
|
41
|
+
directories at 0700.
|
|
42
|
+
- Symlink-following blocked on version marker reads.
|
|
43
|
+
- Cloud-synced directory detection extended to `Library/CloudStorage`
|
|
44
|
+
(macOS 13+).
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Silent error swallows in daemon shutdown, migration probe, and banner
|
|
48
|
+
emission now log at WARNING.
|
|
49
|
+
- Fenced-out `complete()` writes (stale worker claims) emit a WARNING
|
|
50
|
+
log instead of vanishing silently.
|
|
51
|
+
- Daemon-start migration guarded behind `is_ready` sentinel — skips
|
|
52
|
+
when already applied.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
13
56
|
## [3.4.23] - 2026-04-21
|
|
14
57
|
|
|
15
58
|
Critical hotfix on top of 3.4.22 for two end-user-facing regressions.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">SuperLocalMemory V3.4</h1>
|
|
6
6
|
<p align="center"><strong>Every other AI forgets. Yours won't.</strong><br/><em>Infinite memory for Claude Code, Cursor, Windsurf, and any MCP-compatible AI client.</em></p>
|
|
7
|
-
<p align="center"><code>v3.4.
|
|
7
|
+
<p align="center"><code>v3.4.25</code> — Install once. Every session remembers the last. Automatically.</p>
|
|
8
8
|
<p align="center"><strong>Backed by 3 published research papers</strong> (arXiv preprints + Zenodo-archived) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
<a href="https://superlocalmemory.com"><img src="https://img.shields.io/badge/Web-superlocalmemory.com-ff6b35?style=for-the-badge" alt="Website"/></a>
|
|
21
21
|
<a href="#dual-interface-mcp--cli"><img src="https://img.shields.io/badge/MCP-Native-blue?style=for-the-badge" alt="MCP Native"/></a>
|
|
22
22
|
<a href="#dual-interface-mcp--cli"><img src="https://img.shields.io/badge/CLI-Agent--Native-green?style=for-the-badge" alt="CLI Agent-Native"/></a>
|
|
23
|
+
<a href="#multilingual-embedding-support"><img src="https://img.shields.io/badge/Multilingual-30%2B_Languages-ff69b4?style=for-the-badge" alt="Multilingual 30+ Languages"/></a>
|
|
23
24
|
</p>
|
|
24
25
|
|
|
25
26
|
<p align="center">
|
|
@@ -342,6 +343,12 @@ Built-in compliance tools: GDPR Article 15/17 export + complete erasure, tamper-
|
|
|
342
343
|
|
|
343
344
|
---
|
|
344
345
|
|
|
346
|
+
## Multilingual Embedding Support
|
|
347
|
+
|
|
348
|
+
**v3.4.24+:** Plug in any OpenAI-compatible embedding endpoint — Ollama, vLLM, LiteLLM, or self-hosted models like `bge-m3`, `multilingual-e5`, `Qwen3-Embedding`. Configure from the dashboard (Settings > Step 3) or `config.json`. SLM's math layer (Fisher-Rao, Sheaf, Langevin) is language-agnostic — swap the embedding model and all 30+ languages work at full retrieval quality. No cloud dependency. No code changes. Your data, your language, your model.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
345
352
|
## Web Dashboard
|
|
346
353
|
|
|
347
354
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.30",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superlocalmemory"
|
|
3
|
-
version = "3.4.
|
|
3
|
+
version = "3.4.30"
|
|
4
4
|
description = "Information-geometric agent memory with mathematical guarantees"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "AGPL-3.0-or-later"}
|
|
@@ -52,6 +52,8 @@ dependencies = [
|
|
|
52
52
|
# V3.4.3: Unified Brain
|
|
53
53
|
"psutil>=5.9.0",
|
|
54
54
|
"structlog>=24.0.0,<27.0.0",
|
|
55
|
+
# Cross-platform file locking for single-daemon enforcement.
|
|
56
|
+
"portalocker>=2.7.0,<4.0.0",
|
|
55
57
|
# V3.4.18: Semantic search + cross-encoder reranker (npm install parity).
|
|
56
58
|
# Previously under [search] extra — pip users silently lost 30pp of recall
|
|
57
59
|
# quality vs. npm users. Now ships by default for both install paths.
|
|
@@ -12,10 +12,13 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import logging
|
|
15
16
|
import os
|
|
16
17
|
import sys
|
|
17
18
|
from argparse import Namespace
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def _cmd_db_dispatch(args: Namespace) -> None:
|
|
21
24
|
"""Route ``slm db ...`` subcommands. LLD-06 §7.2."""
|
|
@@ -199,7 +199,10 @@ def ensure_daemon() -> bool:
|
|
|
199
199
|
return _wait_for_daemon(timeout=60)
|
|
200
200
|
|
|
201
201
|
except Exception as exc:
|
|
202
|
-
|
|
202
|
+
# Daemon auto-start is the entry point for dashboard / mesh /
|
|
203
|
+
# health features; failure here silently disables all of them.
|
|
204
|
+
# Log at WARNING so operators can see it in production logs.
|
|
205
|
+
logger.warning("ensure_daemon error: %s (run `slm doctor`)", exc)
|
|
203
206
|
return False
|
|
204
207
|
finally:
|
|
205
208
|
if lock_fd:
|
|
@@ -386,8 +389,10 @@ def _flush_observe_buffer() -> None:
|
|
|
386
389
|
decision = auto.evaluate(content)
|
|
387
390
|
if decision.capture:
|
|
388
391
|
auto.capture(content, category=decision.category)
|
|
389
|
-
except Exception:
|
|
390
|
-
|
|
392
|
+
except Exception as exc:
|
|
393
|
+
# Swallow per-observation to protect the batch, but log so
|
|
394
|
+
# a pattern of dropped observations is visible.
|
|
395
|
+
logger.warning("observation dropped during batch: %s", exc)
|
|
391
396
|
|
|
392
397
|
logger.info("Observe debounce: processed %d observations (from buffer)", len(batch))
|
|
393
398
|
|
|
@@ -489,16 +494,55 @@ class DaemonHandler(BaseHTTPRequestHandler):
|
|
|
489
494
|
response = engine.recall(
|
|
490
495
|
query, limit=limit, session_id=session_id,
|
|
491
496
|
)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
for r in response.results
|
|
497
|
-
|
|
497
|
+
# Return the same field shape as recall_worker._handle_recall,
|
|
498
|
+
# so MCP processes that proxy through the daemon get recall_trace-
|
|
499
|
+
# compatible data without a second round trip.
|
|
500
|
+
memory_ids = list({
|
|
501
|
+
r.fact.memory_id for r in response.results[:limit]
|
|
502
|
+
if r.fact.memory_id
|
|
503
|
+
})
|
|
504
|
+
memory_map = (
|
|
505
|
+
engine._db.get_memory_content_batch(memory_ids)
|
|
506
|
+
if memory_ids else {}
|
|
507
|
+
)
|
|
508
|
+
results = []
|
|
509
|
+
for r in response.results[:limit]:
|
|
510
|
+
fact_type = getattr(r.fact, "fact_type", None)
|
|
511
|
+
lifecycle = getattr(r.fact, "lifecycle", None)
|
|
512
|
+
results.append({
|
|
513
|
+
"fact_id": r.fact.fact_id,
|
|
514
|
+
"memory_id": r.fact.memory_id,
|
|
515
|
+
"content": r.fact.content[:300],
|
|
516
|
+
"source_content": memory_map.get(r.fact.memory_id, ""),
|
|
517
|
+
"score": round(r.score, 4),
|
|
518
|
+
"confidence": round(r.confidence, 4),
|
|
519
|
+
"trust_score": round(r.trust_score, 4),
|
|
520
|
+
"channel_scores": {
|
|
521
|
+
k: round(v, 4)
|
|
522
|
+
for k, v in (r.channel_scores or {}).items()
|
|
523
|
+
},
|
|
524
|
+
"fact_type": fact_type.value
|
|
525
|
+
if fact_type and hasattr(fact_type, "value") else "",
|
|
526
|
+
"lifecycle": lifecycle.value
|
|
527
|
+
if lifecycle and hasattr(lifecycle, "value") else "",
|
|
528
|
+
"access_count": getattr(r.fact, "access_count", 0),
|
|
529
|
+
"evidence_chain": list(
|
|
530
|
+
getattr(r, "evidence_chain", []) or []
|
|
531
|
+
),
|
|
532
|
+
})
|
|
498
533
|
self._send_json(200, {
|
|
499
|
-
"
|
|
534
|
+
"ok": True,
|
|
535
|
+
"query": query,
|
|
500
536
|
"query_type": response.query_type,
|
|
537
|
+
"result_count": len(results),
|
|
501
538
|
"retrieval_time_ms": round(response.retrieval_time_ms, 1),
|
|
539
|
+
"channel_weights": {
|
|
540
|
+
k: round(v, 3)
|
|
541
|
+
for k, v in (response.channel_weights or {}).items()
|
|
542
|
+
},
|
|
543
|
+
"total_candidates": getattr(response, "total_candidates", 0),
|
|
544
|
+
"results": results,
|
|
545
|
+
"count": len(results), # backward compat alias
|
|
502
546
|
})
|
|
503
547
|
except Exception as exc:
|
|
504
548
|
self._send_json(500, {"error": str(exc)})
|
|
@@ -541,14 +585,21 @@ class DaemonHandler(BaseHTTPRequestHandler):
|
|
|
541
585
|
body = self._read_body()
|
|
542
586
|
content = body.get("content", "")
|
|
543
587
|
tags = body.get("tags", "")
|
|
588
|
+
extra_meta = body.get("metadata") or {}
|
|
544
589
|
if not content:
|
|
545
590
|
self._send_json(400, {"error": "content required"})
|
|
546
591
|
return
|
|
547
592
|
|
|
548
593
|
engine = _get_engine()
|
|
549
594
|
metadata = {"tags": tags} if tags else {}
|
|
595
|
+
if isinstance(extra_meta, dict):
|
|
596
|
+
metadata.update(extra_meta)
|
|
550
597
|
fact_ids = engine.store(content, metadata=metadata)
|
|
551
|
-
self._send_json(200, {
|
|
598
|
+
self._send_json(200, {
|
|
599
|
+
"ok": True,
|
|
600
|
+
"fact_ids": fact_ids,
|
|
601
|
+
"count": len(fact_ids),
|
|
602
|
+
})
|
|
552
603
|
except Exception as exc:
|
|
553
604
|
self._send_json(500, {"error": str(exc)})
|
|
554
605
|
return
|
|
@@ -589,17 +640,16 @@ _server_start_time = time.monotonic()
|
|
|
589
640
|
|
|
590
641
|
def _shutdown_server() -> None:
|
|
591
642
|
global _engine, _server
|
|
592
|
-
# V3.3.28: Flush any buffered observations before shutdown
|
|
593
643
|
try:
|
|
594
644
|
_flush_observe_buffer()
|
|
595
|
-
except Exception:
|
|
596
|
-
|
|
645
|
+
except Exception as exc:
|
|
646
|
+
logger.warning("flush observe buffer on shutdown failed: %s", exc)
|
|
597
647
|
time.sleep(0.5)
|
|
598
648
|
if _engine is not None:
|
|
599
649
|
try:
|
|
600
650
|
_engine.close()
|
|
601
|
-
except Exception:
|
|
602
|
-
|
|
651
|
+
except Exception as exc:
|
|
652
|
+
logger.warning("engine close on shutdown failed: %s", exc)
|
|
603
653
|
_engine = None
|
|
604
654
|
if _server is not None:
|
|
605
655
|
_server.shutdown()
|
|
@@ -627,6 +677,30 @@ def start_server(port: int = _DEFAULT_PORT, idle_timeout: int | None = None) ->
|
|
|
627
677
|
"SLM_DAEMON_IDLE_TIMEOUT", str(_DEFAULT_IDLE_TIMEOUT),
|
|
628
678
|
))
|
|
629
679
|
|
|
680
|
+
# Banner is advisory — a broken data dir must never prevent the daemon
|
|
681
|
+
# from starting, so the swallow here is intentional.
|
|
682
|
+
try:
|
|
683
|
+
from superlocalmemory import __version__ as _slm_ver
|
|
684
|
+
from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
|
|
685
|
+
check_and_emit_upgrade_banner(_slm_ver)
|
|
686
|
+
except Exception as exc:
|
|
687
|
+
logger.warning("upgrade banner on daemon start failed: %s", exc)
|
|
688
|
+
|
|
689
|
+
# Apply the v3.4.26 data-dir migration now — the daemon is the
|
|
690
|
+
# authoritative holder of the DB, so this is the right place to do
|
|
691
|
+
# it unconditionally (``migrate`` is idempotent).
|
|
692
|
+
try:
|
|
693
|
+
from pathlib import Path as _P
|
|
694
|
+
from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
|
|
695
|
+
is_ready as _is_ready, migrate as _migrate,
|
|
696
|
+
)
|
|
697
|
+
_data = _P(os.environ.get("SLM_DATA_DIR")
|
|
698
|
+
or _P.home() / ".superlocalmemory")
|
|
699
|
+
if not _is_ready(_data):
|
|
700
|
+
_migrate(_data)
|
|
701
|
+
except Exception as exc:
|
|
702
|
+
logger.warning("v3.4.26 migration on daemon start failed: %s", exc)
|
|
703
|
+
|
|
630
704
|
# Write PID + port files
|
|
631
705
|
_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
632
706
|
_PID_FILE.write_text(str(os.getpid()))
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""slm doctor — preflight and health checks.
|
|
6
|
+
|
|
7
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import platform
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
_DEFAULT_DATA_DIR = Path.home() / ".superlocalmemory"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check_python() -> dict[str, Any]:
|
|
22
|
+
v = sys.version_info
|
|
23
|
+
ok = (v.major, v.minor) >= (3, 10)
|
|
24
|
+
return {
|
|
25
|
+
"name": "python",
|
|
26
|
+
"status": "ok" if ok else "error",
|
|
27
|
+
"detail": f"Python {v.major}.{v.minor}.{v.micro}",
|
|
28
|
+
"hint": None if ok else "SLM requires Python >= 3.10",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _check_platform() -> dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"name": "platform",
|
|
35
|
+
"status": "ok",
|
|
36
|
+
"detail": f"{platform.system()} {platform.release()} {platform.machine()}",
|
|
37
|
+
"hint": None,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_portalocker() -> dict[str, Any]:
|
|
42
|
+
try:
|
|
43
|
+
import portalocker # noqa: F401
|
|
44
|
+
return {
|
|
45
|
+
"name": "portalocker",
|
|
46
|
+
"status": "ok",
|
|
47
|
+
"detail": "installed",
|
|
48
|
+
"hint": None,
|
|
49
|
+
}
|
|
50
|
+
except ImportError:
|
|
51
|
+
return {
|
|
52
|
+
"name": "portalocker",
|
|
53
|
+
"status": "error",
|
|
54
|
+
"detail": "not installed",
|
|
55
|
+
"hint": "pip install --upgrade superlocalmemory",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _check_data_dir(data_dir: Path) -> dict[str, Any]:
|
|
60
|
+
from superlocalmemory.core import safe_fs
|
|
61
|
+
try:
|
|
62
|
+
safe_fs.validate_data_dir(data_dir)
|
|
63
|
+
return {
|
|
64
|
+
"name": "data directory",
|
|
65
|
+
"status": "ok",
|
|
66
|
+
"detail": str(data_dir),
|
|
67
|
+
"hint": None,
|
|
68
|
+
}
|
|
69
|
+
except safe_fs.SafeFsError as exc:
|
|
70
|
+
return {
|
|
71
|
+
"name": "data directory",
|
|
72
|
+
"status": "error",
|
|
73
|
+
"detail": str(data_dir),
|
|
74
|
+
"hint": str(exc),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _check_migration(data_dir: Path) -> dict[str, Any]:
|
|
79
|
+
from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import is_ready
|
|
80
|
+
ok = is_ready(data_dir)
|
|
81
|
+
return {
|
|
82
|
+
"name": "v3.4.26 migration",
|
|
83
|
+
"status": "ok" if ok else "warn",
|
|
84
|
+
"detail": "ready" if ok else "not migrated yet",
|
|
85
|
+
"hint": None if ok else
|
|
86
|
+
"Run once: python -m superlocalmemory.migrations.v3_4_25_to_v3_4_26",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _check_queue_db(data_dir: Path) -> dict[str, Any]:
|
|
91
|
+
queue = data_dir / "recall_queue.db"
|
|
92
|
+
if queue.exists():
|
|
93
|
+
return {
|
|
94
|
+
"name": "queue database",
|
|
95
|
+
"status": "ok",
|
|
96
|
+
"detail": f"{queue} ({queue.stat().st_size} bytes)",
|
|
97
|
+
"hint": None,
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
"name": "queue database",
|
|
101
|
+
"status": "warn",
|
|
102
|
+
"detail": "not yet created",
|
|
103
|
+
"hint": "The daemon will create this on first use.",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_checks(*, data_dir: Path | None = None) -> dict[str, Any]:
|
|
108
|
+
dd = Path(data_dir) if data_dir is not None else _DEFAULT_DATA_DIR
|
|
109
|
+
checks = [
|
|
110
|
+
_check_python(),
|
|
111
|
+
_check_platform(),
|
|
112
|
+
_check_portalocker(),
|
|
113
|
+
_check_data_dir(dd),
|
|
114
|
+
_check_migration(dd),
|
|
115
|
+
_check_queue_db(dd),
|
|
116
|
+
]
|
|
117
|
+
has_error = any(c["status"] == "error" for c in checks)
|
|
118
|
+
exit_code = 1 if has_error else 0
|
|
119
|
+
return {
|
|
120
|
+
"data_dir": str(dd),
|
|
121
|
+
"checks": checks,
|
|
122
|
+
"exit_code": exit_code,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _print_report(report: dict[str, Any]) -> None:
|
|
127
|
+
glyphs = {"ok": "\u2713", "warn": "\u26a0", "error": "\u2717"}
|
|
128
|
+
print(f"SLM doctor — data dir: {report['data_dir']}\n")
|
|
129
|
+
for c in report["checks"]:
|
|
130
|
+
g = glyphs.get(c["status"], "?")
|
|
131
|
+
print(f" {g} {c['name']:<20} {c['detail']}")
|
|
132
|
+
if c.get("hint"):
|
|
133
|
+
print(f" {c['hint']}")
|
|
134
|
+
print(f"\nExit code: {report['exit_code']}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main(argv: list[str] | None = None) -> int:
|
|
138
|
+
import argparse
|
|
139
|
+
parser = argparse.ArgumentParser(prog="slm-doctor")
|
|
140
|
+
parser.add_argument("--json", action="store_true", help="JSON output")
|
|
141
|
+
parser.add_argument("--data-dir", type=Path, default=None)
|
|
142
|
+
args = parser.parse_args(argv)
|
|
143
|
+
report = run_checks(data_dir=args.data_dir)
|
|
144
|
+
if args.json:
|
|
145
|
+
print(json.dumps(report, indent=2))
|
|
146
|
+
else:
|
|
147
|
+
_print_report(report)
|
|
148
|
+
return int(report["exit_code"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
raise SystemExit(main())
|
|
@@ -79,6 +79,34 @@ def main() -> None:
|
|
|
79
79
|
from superlocalmemory.cli.json_output import _get_version
|
|
80
80
|
_ver = _get_version()
|
|
81
81
|
|
|
82
|
+
# One-time post-upgrade banner — silent for fresh installs and
|
|
83
|
+
# same-version runs. Guarded against I/O errors internally.
|
|
84
|
+
from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
|
|
85
|
+
if check_and_emit_upgrade_banner(_ver):
|
|
86
|
+
# First post-upgrade invocation: apply the data-dir migration if
|
|
87
|
+
# it's safe. When the previous-version daemon is still running
|
|
88
|
+
# we defer — the next daemon start picks it up.
|
|
89
|
+
try:
|
|
90
|
+
import logging as _logging
|
|
91
|
+
from pathlib import Path as _P
|
|
92
|
+
from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
|
|
93
|
+
migrate_if_safe as _migrate_if_safe,
|
|
94
|
+
)
|
|
95
|
+
_data = _P(_os.environ.get("SLM_DATA_DIR")
|
|
96
|
+
or _P.home() / ".superlocalmemory")
|
|
97
|
+
_res = _migrate_if_safe(_data)
|
|
98
|
+
if _res.get("status") == "deferred":
|
|
99
|
+
print(
|
|
100
|
+
" note: data migration deferred — the running SLM "
|
|
101
|
+
"daemon will apply it on its next restart."
|
|
102
|
+
)
|
|
103
|
+
except Exception as _mig_exc:
|
|
104
|
+
import logging as _logging
|
|
105
|
+
_logging.getLogger(__name__).warning(
|
|
106
|
+
"v3.4.26 migrate_if_safe failed: %s — run `slm doctor`", _mig_exc,
|
|
107
|
+
)
|
|
108
|
+
print(" note: data migration check failed — run `slm doctor` to diagnose.")
|
|
109
|
+
|
|
82
110
|
parser = argparse.ArgumentParser(
|
|
83
111
|
prog="slm",
|
|
84
112
|
description=f"SuperLocalMemory V3 ({_ver}) — AI agent memory with mathematical foundations",
|
|
@@ -21,6 +21,21 @@ def run_post_install():
|
|
|
21
21
|
print("=" * 30)
|
|
22
22
|
print()
|
|
23
23
|
|
|
24
|
+
# Upgrade banner — fires when a prior-version user runs
|
|
25
|
+
# ``npm install -g superlocalmemory@latest``. Silent on fresh
|
|
26
|
+
# installs (setup wizard handles welcome).
|
|
27
|
+
try:
|
|
28
|
+
from superlocalmemory import __version__ as _slm_ver
|
|
29
|
+
from superlocalmemory.cli.version_banner import (
|
|
30
|
+
check_and_emit_upgrade_banner,
|
|
31
|
+
)
|
|
32
|
+
if check_and_emit_upgrade_banner(_slm_ver):
|
|
33
|
+
# Upgrade detected — the banner already covered it;
|
|
34
|
+
# skip the V2 path + fresh-install copy and return.
|
|
35
|
+
return
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
24
39
|
# Step 1: Check for V2 installation
|
|
25
40
|
from superlocalmemory.storage.v2_migrator import V2Migrator
|
|
26
41
|
|
|
@@ -556,6 +556,26 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
556
556
|
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
557
557
|
verified = False
|
|
558
558
|
|
|
559
|
+
# v3.4.26 options — data-dir safety + queue toggle. One prompt max.
|
|
560
|
+
try:
|
|
561
|
+
from superlocalmemory.cli.wizard_v3426_options import (
|
|
562
|
+
persist_v3426_options,
|
|
563
|
+
prompt_v3426_options,
|
|
564
|
+
validate_install_data_dir,
|
|
565
|
+
)
|
|
566
|
+
ok, reason = validate_install_data_dir(_SLM_HOME)
|
|
567
|
+
if not ok:
|
|
568
|
+
print()
|
|
569
|
+
print(" ⚠ Data directory check failed:")
|
|
570
|
+
for line in reason.splitlines():
|
|
571
|
+
print(f" {line}")
|
|
572
|
+
print()
|
|
573
|
+
v3426_opts = prompt_v3426_options(interactive=interactive)
|
|
574
|
+
persist_v3426_options(v3426_opts, _SLM_HOME)
|
|
575
|
+
except Exception as exc:
|
|
576
|
+
# Wizard must never crash over an advisory feature.
|
|
577
|
+
print(f" (v3.4.26 options step skipped: {exc})")
|
|
578
|
+
|
|
559
579
|
# -- Done --
|
|
560
580
|
_mark_complete()
|
|
561
581
|
|