superlocalmemory 3.4.25 → 3.4.31
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 +92 -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/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/pending_store.py +55 -3
- 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/maintenance_scheduler.py +8 -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_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/learning/signals.py +7 -1
- 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/data_io.py +21 -2
- package/src/superlocalmemory/server/routes/memories.py +91 -0
- package/src/superlocalmemory/server/routes/stats.py +16 -2
- package/src/superlocalmemory/server/unified_daemon.py +128 -12
- package/src/superlocalmemory/ui/index.html +35 -25
- package/src/superlocalmemory/ui/js/core.js +20 -4
- package/src/superlocalmemory/ui/js/fact-detail.js +62 -73
- package/src/superlocalmemory/ui/js/memories.js +34 -2
- package/src/superlocalmemory/ui/js/modal.js +41 -2
- package/src/superlocalmemory/ui/js/search.js +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,98 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [3.4.31] - 2026-04-24
|
|
14
|
+
|
|
15
|
+
Dashboard truth, memory vs fact clarity, and self-cleaning pending queue.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Dashboard now shows both memory counts honestly.** Parent memories
|
|
19
|
+
(what you stored) and atomic facts (what retrieval indexes) appear as
|
|
20
|
+
two distinct cards with their ratio. No more "Total Memories: 6,000"
|
|
21
|
+
when you actually have 2,000 memories decomposed into 6,000 facts.
|
|
22
|
+
- **"Browse atomic facts"** relabeled for clarity — this view lists the
|
|
23
|
+
indexed atomic units.
|
|
24
|
+
- **Visible search box** in the Memories tab — previously hidden behind
|
|
25
|
+
the Recall Lab only. Search now debounces 280 ms on input.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`/api/memories/{id}/detail`** — full memory + all child atomic facts
|
|
29
|
+
in one call. Powers the click-to-expand modal.
|
|
30
|
+
- **`/api/facts/{id}`** — single atomic fact detail with source memory
|
|
31
|
+
content, entities, and canonical entities.
|
|
32
|
+
- **Pagination UI** — Prev/Next controls show "Showing 1–50 of 6,123".
|
|
33
|
+
Previously hardcoded to 50 with no navigation.
|
|
34
|
+
- **CSV export** — new `format=csv` option on `/api/export` plus a
|
|
35
|
+
dedicated "Export All (CSV)" menu item. JSON and JSONL still work.
|
|
36
|
+
- **Export progress toast** — "Preparing JSON export…" notification
|
|
37
|
+
before the download starts.
|
|
38
|
+
- **`total_facts` + `facts_per_memory`** in `/api/stats` response.
|
|
39
|
+
- **Pending queue auto-cleanup** — the maintenance scheduler now sweeps
|
|
40
|
+
the pending queue every cycle: completed rows > 7 days, failed rows
|
|
41
|
+
over retry limit, and stuck rows > 7 days are removed; a 30-day hard
|
|
42
|
+
cap prevents runaway growth on any status.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- **Test isolation** — `pending_store` now honors `SLM_DATA_DIR`. Four
|
|
46
|
+
MCP remember tests were writing to the live `~/.superlocalmemory/`
|
|
47
|
+
instead of `tmp_path`. Root conftest now forces `SLM_DATA_DIR=tmp_path`
|
|
48
|
+
for every test unless explicitly opted out.
|
|
49
|
+
- **Fact click popup** — was calling `/api/v3/recall/trace` with a text
|
|
50
|
+
substring (re-query by first 100 chars) and colliding with the memory
|
|
51
|
+
row click handler. Now scoped to `.fact-result-item` only, hits the
|
|
52
|
+
new `/api/facts/{fact_id}` endpoint.
|
|
53
|
+
- **Memory modal ID confusion** — the modal labeled `mem.id` as "ID"
|
|
54
|
+
regardless of whether it was a memory_id or fact_id. Now displays
|
|
55
|
+
both "Memory ID" and "Fact ID" when they differ.
|
|
56
|
+
- **Memory modal hydration** — fetches the full memory + fact list
|
|
57
|
+
asynchronously when opened, so source content and entity data appear
|
|
58
|
+
even for rows that arrived from the search endpoint.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## [3.4.30] - 2026-04-24
|
|
63
|
+
|
|
64
|
+
Multi-IDE shared worker, silent migration, and security hardening.
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- **Multi-IDE RAM sharing.** MCP processes share a single recall worker
|
|
68
|
+
via the daemon. Total RSS stays below 2 GB with four IDEs open.
|
|
69
|
+
- **Feedback and learning signals** flow from every IDE session to the
|
|
70
|
+
daemon, not just the first.
|
|
71
|
+
- **Setup wizard** validates the data directory at install time and
|
|
72
|
+
rejects iCloud, Dropbox, OneDrive, Box, Google Drive, and
|
|
73
|
+
`Library/CloudStorage` paths that silently corrupt SQLite WAL.
|
|
74
|
+
- **One-time upgrade banner** after `pip install -U` / `npm install -g`
|
|
75
|
+
points users to `slm doctor`.
|
|
76
|
+
- **`docs/errors.md`** — canonical error catalog with codes, recovery
|
|
77
|
+
steps, exit codes, and HTTP status mappings.
|
|
78
|
+
- **CI matrix** now runs on `ubuntu-22.04`, `macos-14` (Apple Silicon),
|
|
79
|
+
and `windows-latest` with `portalocker`.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- **Silent, atomic data migration** on upgrade — no manual steps.
|
|
83
|
+
- **Migration serialized via file lock** so parallel pip + npm installs
|
|
84
|
+
cannot race.
|
|
85
|
+
- **Concurrent-safe MCP engine singleton** with double-checked locking.
|
|
86
|
+
- Pool adapter returns frozen dataclasses instead of `SimpleNamespace`.
|
|
87
|
+
|
|
88
|
+
### Security
|
|
89
|
+
- File permissions tightened: marker files written at 0600, parent
|
|
90
|
+
directories at 0700.
|
|
91
|
+
- Symlink-following blocked on version marker reads.
|
|
92
|
+
- Cloud-synced directory detection extended to `Library/CloudStorage`
|
|
93
|
+
(macOS 13+).
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
- Silent error swallows in daemon shutdown, migration probe, and banner
|
|
97
|
+
emission now log at WARNING.
|
|
98
|
+
- Fenced-out `complete()` writes (stale worker claims) emit a WARNING
|
|
99
|
+
log instead of vanishing silently.
|
|
100
|
+
- Daemon-start migration guarded behind `is_ready` sentinel — skips
|
|
101
|
+
when already applied.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
13
105
|
## [3.4.23] - 2026-04-21
|
|
14
106
|
|
|
15
107
|
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.31",
|
|
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.31"
|
|
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.
|
|
@@ -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",
|
|
@@ -24,12 +24,21 @@ License: Elastic-2.0
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
26
|
import json
|
|
27
|
+
import os
|
|
27
28
|
import sqlite3
|
|
28
29
|
import time
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
def _default_dir() -> Path:
|
|
34
|
+
"""Honor SLM_DATA_DIR so tests can isolate via tmp_path."""
|
|
35
|
+
return Path(os.environ.get("SLM_DATA_DIR") or Path.home() / ".superlocalmemory")
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
_PENDING_DB = "pending.db"
|
|
39
|
+
_MAX_RETRIES = 3
|
|
40
|
+
_STUCK_DAYS = 7
|
|
41
|
+
_DEAD_LETTER_DAYS = 30
|
|
33
42
|
|
|
34
43
|
_SCHEMA = """
|
|
35
44
|
CREATE TABLE IF NOT EXISTS pending_memories (
|
|
@@ -47,7 +56,7 @@ CREATE TABLE IF NOT EXISTS pending_memories (
|
|
|
47
56
|
|
|
48
57
|
def _get_db(base_dir: Path | None = None) -> sqlite3.Connection:
|
|
49
58
|
"""Open pending.db with WAL mode. Creates if needed."""
|
|
50
|
-
d = base_dir or
|
|
59
|
+
d = base_dir or _default_dir()
|
|
51
60
|
d.mkdir(parents=True, exist_ok=True)
|
|
52
61
|
db_path = d / _PENDING_DB
|
|
53
62
|
conn = sqlite3.connect(str(db_path), timeout=5)
|
|
@@ -128,7 +137,7 @@ def mark_failed(row_id: int, error: str, base_dir: Path | None = None) -> None:
|
|
|
128
137
|
|
|
129
138
|
def pending_count(base_dir: Path | None = None) -> int:
|
|
130
139
|
"""Count unprocessed pending memories."""
|
|
131
|
-
d = base_dir or
|
|
140
|
+
d = base_dir or _default_dir()
|
|
132
141
|
db_path = d / _PENDING_DB
|
|
133
142
|
if not db_path.exists():
|
|
134
143
|
return 0
|
|
@@ -156,3 +165,46 @@ def cleanup_done(days: int = 7, base_dir: Path | None = None) -> int:
|
|
|
156
165
|
return cur.rowcount
|
|
157
166
|
finally:
|
|
158
167
|
conn.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def cleanup_stale(base_dir: Path | None = None) -> dict[str, int]:
|
|
171
|
+
"""Sweep stale rows from pending.db. Runs periodically from the daemon.
|
|
172
|
+
|
|
173
|
+
Removes:
|
|
174
|
+
- `done` rows older than 7 days (already processed)
|
|
175
|
+
- `failed` rows that exceeded max retries (moved to dead-letter via deletion)
|
|
176
|
+
- `pending` rows stuck more than 7 days (test pollution, crashed workers)
|
|
177
|
+
- Everything older than 30 days regardless of status (hard cap)
|
|
178
|
+
"""
|
|
179
|
+
conn = _get_db(base_dir)
|
|
180
|
+
try:
|
|
181
|
+
done = conn.execute(
|
|
182
|
+
"DELETE FROM pending_memories WHERE status = 'done' "
|
|
183
|
+
"AND created_at < datetime('now', ?)",
|
|
184
|
+
(f"-{_STUCK_DAYS} days",),
|
|
185
|
+
).rowcount
|
|
186
|
+
failed = conn.execute(
|
|
187
|
+
"DELETE FROM pending_memories WHERE status = 'failed' "
|
|
188
|
+
"AND retry_count >= ?",
|
|
189
|
+
(_MAX_RETRIES,),
|
|
190
|
+
).rowcount
|
|
191
|
+
stuck = conn.execute(
|
|
192
|
+
"DELETE FROM pending_memories WHERE status = 'pending' "
|
|
193
|
+
"AND created_at < datetime('now', ?)",
|
|
194
|
+
(f"-{_STUCK_DAYS} days",),
|
|
195
|
+
).rowcount
|
|
196
|
+
hard_cap = conn.execute(
|
|
197
|
+
"DELETE FROM pending_memories "
|
|
198
|
+
"WHERE created_at < datetime('now', ?)",
|
|
199
|
+
(f"-{_DEAD_LETTER_DAYS} days",),
|
|
200
|
+
).rowcount
|
|
201
|
+
conn.commit()
|
|
202
|
+
return {
|
|
203
|
+
"done": done,
|
|
204
|
+
"failed_over_retries": failed,
|
|
205
|
+
"stuck_pending": stuck,
|
|
206
|
+
"hard_cap_expired": hard_cap,
|
|
207
|
+
"total": done + failed + stuck + hard_cap,
|
|
208
|
+
}
|
|
209
|
+
finally:
|
|
210
|
+
conn.close()
|
|
@@ -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
|
|