superlocalmemory 3.6.20 → 3.6.22
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 +4 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/llm/backbone.py +9 -2
- package/src/superlocalmemory/mcp/tools_v3.py +4 -16
- package/src/superlocalmemory/server/routes/mesh.py +12 -4
- package/src/superlocalmemory/server/routes/v3_api.py +59 -47
- package/src/superlocalmemory/ui/js/auto-settings.js +8 -11
- package/src/superlocalmemory/ui/js/clusters.js +1 -0
- package/src/superlocalmemory/ui/js/compliance.js +1 -0
- package/src/superlocalmemory/ui/js/core.js +3 -0
- package/src/superlocalmemory/ui/js/dashboard.js +23 -6
- package/src/superlocalmemory/ui/js/knowledge-graph.js +4 -1
- package/src/superlocalmemory/ui/js/math-health.js +5 -1
- package/src/superlocalmemory/ui/js/memories.js +1 -0
- package/src/superlocalmemory/ui/js/memory-chat.js +1 -1
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -2
- package/src/superlocalmemory/ui/js/ng-mesh.js +123 -21
- package/src/superlocalmemory/ui/js/optimize.js +6 -1
- package/src/superlocalmemory/ui/js/profiles.js +1 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +9 -3
- package/src/superlocalmemory/ui/js/timeline.js +3 -2
- package/src/superlocalmemory.egg-info/PKG-INFO +5 -3
- package/src/superlocalmemory.egg-info/SOURCES.txt +1 -0
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory" width="200"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">SuperLocalMemory V3.6.
|
|
5
|
+
<h1 align="center">SuperLocalMemory V3.6.21</h1>
|
|
6
6
|
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
|
|
7
7
|
<em>To the best of our knowledge, the only zero-cloud agent memory that beats Mem0's zero-LLM score on LoCoMo. Mode A: 74.8% vs Mem0 64.2% — no GPU, no API key, on CPU.</em></p>
|
|
8
|
-
<p align="center"><code>v3.6.
|
|
8
|
+
<p align="center"><code>v3.6.21</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
|
|
9
9
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
10
10
|
<p align="center"><strong>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>
|
|
11
11
|
|
|
@@ -307,6 +307,8 @@ slm dashboard # Opens at http://localhost:8765
|
|
|
307
307
|
|
|
308
308
|
| Version | Codename | Key Features |
|
|
309
309
|
|---|---|---|
|
|
310
|
+
| **v3.6.21** | Dashboard Audit | Full UI audit across all 7 dashboard tabs — auth fix for mesh panel (issue #60 frontend), Quick Store endpoint, timeline endpoint, r.ok guards, SSE \r fix, event delegation for lazy tabs, optimize toggle revert; backbone.py JSONDecodeError fix (issue #62) |
|
|
311
|
+
| **v3.6.20** | Mesh Auth | Remote mesh auth fix (issue #60) — `_get_broker` now accepts Bearer + X-Mesh-Secret from non-loopback callers; config settings preservation (AIDEV-86) |
|
|
310
312
|
| **v3.6.17** | Community | 8 contributor PRs (observability events, marker-bounded adapter writes, daemon port discovery, anthropic `api_base`, OpenMP workers, atomic-write rehash, `_jl` sentinel, LFS pointer); dashboard-feedback fix (#53/#59); env-tunable SQLite knobs + idle backoff; remote LLM test-probe (#40) |
|
|
311
313
|
| **v3.6.16** | Docs | Corrected Claude Code plugin install — adds the required `/plugin marketplace add` step; clarifies plugin vs pip/npm delivery |
|
|
312
314
|
| **v3.6.15** | Multi-scope | **Opt-in [shared memory](docs/shared-memory.md)** (personal/shared/global, off by default), default-deny scope at every read path, recall scope-race fix, contributor PRs #42/#43/#44, fixes #46–#49 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.22",
|
|
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/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.6.
|
|
1
|
+
superlocalmemory==3.6.22
|
package/plugin-src/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.6.
|
|
1
|
+
superlocalmemory==3.6.22
|
package/pyproject.toml
CHANGED
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "3.6.
|
|
35
|
+
__version__ = "3.6.22"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -190,7 +190,7 @@ class LLMBackbone:
|
|
|
190
190
|
logger.warning("Content filter or bad request (400). Returning empty.")
|
|
191
191
|
return ""
|
|
192
192
|
last_error = exc
|
|
193
|
-
except (httpx.TimeoutException, httpx.ConnectError) as exc:
|
|
193
|
+
except (httpx.TimeoutException, httpx.ConnectError, ValueError) as exc:
|
|
194
194
|
last_error = exc
|
|
195
195
|
|
|
196
196
|
if attempt < _MAX_RETRIES - 1:
|
|
@@ -217,7 +217,14 @@ class LLMBackbone:
|
|
|
217
217
|
with httpx.Client(timeout=timeout) as client:
|
|
218
218
|
resp = client.post(url, headers=headers, json=payload)
|
|
219
219
|
resp.raise_for_status()
|
|
220
|
-
|
|
220
|
+
try:
|
|
221
|
+
return resp.json()
|
|
222
|
+
except Exception as exc:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"LLM endpoint returned HTTP {resp.status_code} with "
|
|
225
|
+
f"non-JSON body (Content-Length="
|
|
226
|
+
f"{resp.headers.get('content-length', '?')}): {exc}"
|
|
227
|
+
) from exc
|
|
221
228
|
finally:
|
|
222
229
|
socket.setdefaulttimeout(old_default)
|
|
223
230
|
|
|
@@ -65,25 +65,13 @@ def register_v3_tools(server, get_engine: Callable) -> None:
|
|
|
65
65
|
"error": f"Invalid mode '{mode}'. Use 'a', 'b', or 'c'.",
|
|
66
66
|
}
|
|
67
67
|
from superlocalmemory.core.config import SLMConfig
|
|
68
|
-
from superlocalmemory.storage.models import Mode
|
|
69
68
|
from superlocalmemory.mcp.server import reset_engine
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
# Use switch_mode() — the correct load-then-patch path that preserves
|
|
71
|
+
# all user-tuned config blocks (forgetting, injection, retrieval, scope,
|
|
72
|
+
# math, channel_weights, …). for_mode() resets them to hardcoded defaults.
|
|
72
73
|
old_config = SLMConfig.load()
|
|
73
|
-
config = SLMConfig.
|
|
74
|
-
mode_enum,
|
|
75
|
-
llm_provider=old_config.llm.provider,
|
|
76
|
-
llm_model=old_config.llm.model,
|
|
77
|
-
llm_api_key=old_config.llm.api_key,
|
|
78
|
-
llm_api_base=old_config.llm.api_base,
|
|
79
|
-
embedding_provider=old_config.embedding.provider,
|
|
80
|
-
embedding_endpoint=old_config.embedding.api_endpoint,
|
|
81
|
-
embedding_key=old_config.embedding.api_key,
|
|
82
|
-
embedding_model_name=old_config.embedding.model_name,
|
|
83
|
-
embedding_dimension=old_config.embedding.dimension,
|
|
84
|
-
)
|
|
85
|
-
config.active_profile = old_config.active_profile
|
|
86
|
-
config.save(mode_change=True)
|
|
74
|
+
config = SLMConfig.switch_mode(mode_lower)
|
|
87
75
|
|
|
88
76
|
# V3.3: Check if embedding model changed — flag for re-indexing
|
|
89
77
|
needs_reindex = (
|
|
@@ -87,15 +87,23 @@ def _get_broker(request: Request):
|
|
|
87
87
|
client_host = request.client.host if request.client else ""
|
|
88
88
|
if client_host not in ("127.0.0.1", "::1", "localhost"):
|
|
89
89
|
import hmac
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
90
|
+
from superlocalmemory.core.security_primitives import verify_install_token
|
|
91
|
+
|
|
92
|
+
# Path 1: install token — dashboard/browser callers hold this and
|
|
93
|
+
# should not need the mesh secret exposed in JS.
|
|
94
|
+
install_token = request.headers.get("x-install-token", "")
|
|
95
|
+
if install_token and verify_install_token(install_token):
|
|
96
|
+
return broker
|
|
97
|
+
|
|
98
|
+
# Path 2: mesh secret — remote agents / remote_sync.py / LAN peers.
|
|
99
|
+
# Accept X-Mesh-Secret (legacy v3.6.12 header) OR
|
|
100
|
+
# Authorization: Bearer <secret> (RFC 7617 canonical form).
|
|
93
101
|
presented = (
|
|
94
102
|
request.headers.get("x-mesh-secret")
|
|
95
103
|
or request.headers.get("authorization", "").removeprefix("Bearer ").strip()
|
|
96
104
|
)
|
|
97
105
|
if not presented or not hmac.compare_digest(presented, secret):
|
|
98
|
-
raise HTTPException(401, detail="invalid or missing
|
|
106
|
+
raise HTTPException(401, detail="invalid or missing credential")
|
|
99
107
|
return broker
|
|
100
108
|
|
|
101
109
|
|
|
@@ -123,20 +123,17 @@ async def set_mode(request: Request):
|
|
|
123
123
|
status_code=400,
|
|
124
124
|
)
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
new_config.active_profile = old_config.active_profile
|
|
139
|
-
new_config.save(mode_change=True)
|
|
126
|
+
# Apply new mode's structural presets (retrieval, math, channel_weights)
|
|
127
|
+
# by building a fresh template, then graft them onto the loaded config so
|
|
128
|
+
# all user-tuned blocks (forgetting, injection, consolidation, scope, …)
|
|
129
|
+
# are preserved across the mode switch.
|
|
130
|
+
_template = SLMConfig.for_mode(Mode(new_mode))
|
|
131
|
+
old_config.mode = Mode(new_mode)
|
|
132
|
+
old_config.retrieval = _template.retrieval
|
|
133
|
+
old_config.math = _template.math
|
|
134
|
+
old_config.channel_weights = _template.channel_weights
|
|
135
|
+
old_config.save(mode_change=True)
|
|
136
|
+
new_config = old_config
|
|
140
137
|
|
|
141
138
|
# Audit the change before we lose context — proves who/when/what.
|
|
142
139
|
# Captures the phantom-write case where `for_mode(C)` auto-defaults
|
|
@@ -184,35 +181,52 @@ async def set_full_config(request: Request):
|
|
|
184
181
|
if new_mode not in ("a", "b", "c"):
|
|
185
182
|
return JSONResponse({"error": "Invalid mode"}, status_code=400)
|
|
186
183
|
|
|
187
|
-
from superlocalmemory.core.config import SLMConfig
|
|
184
|
+
from superlocalmemory.core.config import SLMConfig, EmbeddingConfig, LLMConfig
|
|
188
185
|
from superlocalmemory.storage.models import Mode
|
|
189
186
|
from superlocalmemory.server.routes.helpers import log_mode_change
|
|
190
|
-
|
|
191
|
-
old_mode =
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
# llama.cpp / LM Studio / Azure-OpenAI endpoint configured in the
|
|
195
|
-
# dashboard could never be saved (Test Connection then probed the wrong
|
|
196
|
-
# URL → 401). Accept both base_url and endpoint; default ollama locally.
|
|
187
|
+
config = SLMConfig.load()
|
|
188
|
+
old_mode = config.mode.value
|
|
189
|
+
|
|
190
|
+
# v3.6.12 (settings-2): honor a custom endpoint for ANY provider.
|
|
197
191
|
_endpoint = (body.get("base_url", "") or body.get("endpoint", "")).strip()
|
|
198
192
|
if not _endpoint and provider == "ollama":
|
|
199
193
|
_endpoint = "http://localhost:11434"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
embedding_dimension=int(body.get("embedding_dimension", 0) or 0),
|
|
194
|
+
|
|
195
|
+
# Mutate only the fields the dashboard sent — all other config blocks
|
|
196
|
+
# (forgetting, injection, retrieval, math, consolidation, scope, …) are
|
|
197
|
+
# preserved because we loaded the full existing config above.
|
|
198
|
+
config.mode = Mode(new_mode)
|
|
199
|
+
config.llm = LLMConfig(
|
|
200
|
+
provider=provider if provider != "none" else "",
|
|
201
|
+
model=model,
|
|
202
|
+
api_key=api_key,
|
|
203
|
+
api_base=_endpoint,
|
|
211
204
|
)
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
|
|
205
|
+
|
|
206
|
+
# Update embedding only when the dashboard explicitly sent those fields;
|
|
207
|
+
# absence means "leave it alone" (AIDEV-86 / broader fix).
|
|
208
|
+
_emb_fields = ("embedding_provider", "embedding_endpoint", "embedding_key",
|
|
209
|
+
"embedding_model", "embedding_dimension")
|
|
210
|
+
if any(k in body for k in _emb_fields):
|
|
211
|
+
config.embedding = EmbeddingConfig(
|
|
212
|
+
provider=body.get("embedding_provider", ""),
|
|
213
|
+
api_endpoint=body.get("embedding_endpoint", ""),
|
|
214
|
+
api_key=body.get("embedding_key", ""),
|
|
215
|
+
model_name=body.get("embedding_model", ""),
|
|
216
|
+
dimension=int(body.get("embedding_dimension", 0) or 0),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# When the mode actually changed, apply the new mode's structural presets
|
|
220
|
+
# (retrieval topology, math thresholds, channel weights) so the user gets
|
|
221
|
+
# the right runtime behaviour for their chosen mode.
|
|
222
|
+
if new_mode != old_mode:
|
|
223
|
+
_template = SLMConfig.for_mode(Mode(new_mode))
|
|
224
|
+
config.retrieval = _template.retrieval
|
|
225
|
+
config.math = _template.math
|
|
226
|
+
config.channel_weights = _template.channel_weights
|
|
227
|
+
|
|
228
|
+
# v3.6.12 (settings-1): mode_change=True is required to persist the new
|
|
229
|
+
# mode — save() without it hits a guard that preserves the old mode.
|
|
216
230
|
config.save(mode_change=True)
|
|
217
231
|
|
|
218
232
|
log_mode_change(
|
|
@@ -607,8 +621,7 @@ async def set_provider(request: Request):
|
|
|
607
621
|
model = body.get("model", "")
|
|
608
622
|
base_url = body.get("base_url", "")
|
|
609
623
|
|
|
610
|
-
from superlocalmemory.core.config import SLMConfig
|
|
611
|
-
from superlocalmemory.storage.models import Mode
|
|
624
|
+
from superlocalmemory.core.config import SLMConfig, LLMConfig
|
|
612
625
|
config = SLMConfig.load()
|
|
613
626
|
|
|
614
627
|
# Use preset base_url if not provided
|
|
@@ -619,15 +632,14 @@ async def set_provider(request: Request):
|
|
|
619
632
|
if not model:
|
|
620
633
|
model = preset.get("model", "")
|
|
621
634
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
635
|
+
# Mutate only the LLM block — all other config is preserved.
|
|
636
|
+
config.llm = LLMConfig(
|
|
637
|
+
provider=provider,
|
|
638
|
+
model=model,
|
|
639
|
+
api_key=api_key,
|
|
640
|
+
api_base=base_url,
|
|
628
641
|
)
|
|
629
|
-
|
|
630
|
-
new_config.save()
|
|
642
|
+
config.save()
|
|
631
643
|
|
|
632
644
|
return {"success": True, "provider": provider, "model": model}
|
|
633
645
|
except Exception as e:
|
|
@@ -52,17 +52,14 @@ function saveAutoRecallConfig() {
|
|
|
52
52
|
}).catch(function(e) { console.log('Save auto-recall error:', e); });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
document.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
document.querySelectorAll('#auto-recall-toggle, #auto-recall-session').forEach(function(el) {
|
|
64
|
-
if (el) {
|
|
65
|
-
el.addEventListener('change', saveAutoRecallConfig);
|
|
55
|
+
// Use delegation so listeners work even when the settings pane is injected lazily.
|
|
56
|
+
document.addEventListener('change', function(e) {
|
|
57
|
+
var id = e.target && e.target.id;
|
|
58
|
+
if (!id) return;
|
|
59
|
+
if (id === 'auto-capture-toggle' || id === 'auto-capture-decisions' || id === 'auto-capture-bugs') {
|
|
60
|
+
saveAutoCaptureConfig();
|
|
61
|
+
} else if (id === 'auto-recall-toggle' || id === 'auto-recall-session') {
|
|
62
|
+
saveAutoRecallConfig();
|
|
66
63
|
}
|
|
67
64
|
});
|
|
68
65
|
|
|
@@ -117,6 +117,7 @@ async function loadClusters() {
|
|
|
117
117
|
}
|
|
118
118
|
try {
|
|
119
119
|
var response = await fetch('/api/clusters');
|
|
120
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
120
121
|
var data = await response.json();
|
|
121
122
|
_CLUSTERS_STATE.clusters = data.clusters || [];
|
|
122
123
|
_CLUSTERS_STATE.page = 0;
|
|
@@ -13,6 +13,7 @@ async function loadCompliance() {
|
|
|
13
13
|
var url = '/api/compliance/status';
|
|
14
14
|
if (filterValue) url += '?event_type=' + encodeURIComponent(filterValue);
|
|
15
15
|
var response = await fetch(url);
|
|
16
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
16
17
|
var data = await response.json();
|
|
17
18
|
_complianceData = data;
|
|
18
19
|
|
|
@@ -416,6 +416,9 @@ function refreshDashboard() {
|
|
|
416
416
|
function populateFilters(categories, projects) {
|
|
417
417
|
var categorySelect = document.getElementById('filter-category');
|
|
418
418
|
var projectSelect = document.getElementById('filter-project');
|
|
419
|
+
// Clear existing options beyond the first placeholder to prevent duplicates on refresh
|
|
420
|
+
if (categorySelect) while (categorySelect.options.length > 1) categorySelect.remove(1);
|
|
421
|
+
if (projectSelect) while (projectSelect.options.length > 1) projectSelect.remove(1);
|
|
419
422
|
categories.forEach(function(cat) {
|
|
420
423
|
if (cat.category) {
|
|
421
424
|
var option = document.createElement('option');
|
|
@@ -63,7 +63,12 @@ document.addEventListener('click', function(e) {
|
|
|
63
63
|
method: 'PUT',
|
|
64
64
|
headers: {'Content-Type': 'application/json'},
|
|
65
65
|
body: JSON.stringify({mode: mode})
|
|
66
|
-
}).then(function() {
|
|
66
|
+
}).then(function(r) {
|
|
67
|
+
if (!r.ok) return r.json().catch(function() { return {}; }).then(function(d) {
|
|
68
|
+
showToast('Mode switch failed: ' + (d.error || d.detail || r.status), 'error');
|
|
69
|
+
});
|
|
70
|
+
return r.json().then(function() { loadDashboard(); });
|
|
71
|
+
}).catch(function() { showToast('Mode switch failed: network error', 'error'); });
|
|
67
72
|
}
|
|
68
73
|
});
|
|
69
74
|
|
|
@@ -72,15 +77,21 @@ document.getElementById('quick-store-btn')?.addEventListener('click', function()
|
|
|
72
77
|
var input = document.getElementById('quick-store-input');
|
|
73
78
|
var content = input.value.trim();
|
|
74
79
|
if (!content) return;
|
|
75
|
-
fetch('/
|
|
80
|
+
fetch('/remember', {
|
|
76
81
|
method: 'POST',
|
|
77
82
|
headers: {'Content-Type': 'application/json'},
|
|
78
83
|
body: JSON.stringify({content: content})
|
|
79
|
-
}).then(function(r) {
|
|
84
|
+
}).then(function(r) {
|
|
85
|
+
if (!r.ok) return r.json().catch(function() { return {}; }).then(function(d) {
|
|
86
|
+
showToast('Store failed: ' + (d.detail || d.error || r.status), 'error');
|
|
87
|
+
});
|
|
88
|
+
return r.json();
|
|
89
|
+
}).then(function(data) {
|
|
90
|
+
if (!data) return;
|
|
80
91
|
input.value = '';
|
|
81
92
|
loadDashboard();
|
|
82
|
-
|
|
83
|
-
});
|
|
93
|
+
showToast('Stored!');
|
|
94
|
+
}).catch(function() { showToast('Store failed: network error', 'error'); });
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
// Quick recall
|
|
@@ -91,7 +102,10 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
|
|
|
91
102
|
method: 'POST',
|
|
92
103
|
headers: {'Content-Type': 'application/json'},
|
|
93
104
|
body: JSON.stringify({query: query, limit: 5})
|
|
94
|
-
}).then(function(r) {
|
|
105
|
+
}).then(function(r) {
|
|
106
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
107
|
+
return r.json();
|
|
108
|
+
}).then(function(data) {
|
|
95
109
|
var div = document.getElementById('quick-recall-results');
|
|
96
110
|
if (!data.results || data.results.length === 0) {
|
|
97
111
|
div.textContent = 'No results found.';
|
|
@@ -113,5 +127,8 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
|
|
|
113
127
|
row.appendChild(scoreSpan);
|
|
114
128
|
div.appendChild(row);
|
|
115
129
|
});
|
|
130
|
+
}).catch(function(e) {
|
|
131
|
+
var div = document.getElementById('quick-recall-results');
|
|
132
|
+
if (div) div.textContent = 'Search failed. Is the daemon running?';
|
|
116
133
|
});
|
|
117
134
|
});
|
|
@@ -618,7 +618,10 @@ function loadGraphSigma() {
|
|
|
618
618
|
if (typeof showLoadingSpinner === 'function') showLoadingSpinner();
|
|
619
619
|
|
|
620
620
|
fetch('/api/graph?max_nodes=' + fetchLimit + '&min_importance=' + minImportance)
|
|
621
|
-
.then(function(r) {
|
|
621
|
+
.then(function(r) {
|
|
622
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
623
|
+
return r.json();
|
|
624
|
+
})
|
|
622
625
|
.then(function(data) {
|
|
623
626
|
// Store in shared globals
|
|
624
627
|
if (typeof window.graphData !== 'undefined') window.graphData = data;
|
|
@@ -61,7 +61,11 @@ async function loadMathHealth() {
|
|
|
61
61
|
var liStatus = document.createElement('li');
|
|
62
62
|
liStatus.appendChild(document.createTextNode('Status: '));
|
|
63
63
|
var statusBadge = document.createElement('span');
|
|
64
|
-
|
|
64
|
+
var _s = (layer.status || 'active').toLowerCase();
|
|
65
|
+
var _badgeClass = _s === 'error' || _s === 'critical' ? 'bg-danger' :
|
|
66
|
+
_s === 'warning' || _s === 'degraded' ? 'bg-warning text-dark' :
|
|
67
|
+
'bg-success';
|
|
68
|
+
statusBadge.className = 'badge ' + _badgeClass;
|
|
65
69
|
statusBadge.textContent = layer.status || 'active';
|
|
66
70
|
liStatus.appendChild(statusBadge);
|
|
67
71
|
ul.appendChild(liStatus);
|
|
@@ -41,6 +41,7 @@ async function loadMemories(page) {
|
|
|
41
41
|
showLoading('memories-list', 'Loading memories...');
|
|
42
42
|
try {
|
|
43
43
|
var response = await fetch(url);
|
|
44
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
44
45
|
var data = await response.json();
|
|
45
46
|
lastSearchResults = null;
|
|
46
47
|
var exportBtn = document.getElementById('export-search-btn');
|
|
@@ -157,7 +157,7 @@ function sendChatQuery(query) {
|
|
|
157
157
|
|
|
158
158
|
var currentEvent = '';
|
|
159
159
|
for (var i = 0; i < lines.length; i++) {
|
|
160
|
-
var line = lines[i];
|
|
160
|
+
var line = lines[i].replace(/\r$/, ''); // strip \r from \r\n streams
|
|
161
161
|
if (line.startsWith('event: ')) {
|
|
162
162
|
currentEvent = line.substring(7).trim();
|
|
163
163
|
} else if (line.startsWith('data: ')) {
|
|
@@ -16,12 +16,16 @@
|
|
|
16
16
|
function fetchEntityList(offset) {
|
|
17
17
|
offset = offset || 0;
|
|
18
18
|
fetch('/api/entity/list?limit=' + PAGE_SIZE + '&offset=' + offset)
|
|
19
|
-
.then(function(r) {
|
|
19
|
+
.then(function(r) {
|
|
20
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
21
|
+
return r.json();
|
|
22
|
+
})
|
|
20
23
|
.then(function(data) {
|
|
21
24
|
allEntities = data.entities || [];
|
|
22
25
|
renderEntityList(allEntities, data.total || 0, offset);
|
|
23
26
|
})
|
|
24
|
-
.catch(function() {
|
|
27
|
+
.catch(function(err) {
|
|
28
|
+
console.error('[entities] fetchEntityList failed:', err);
|
|
25
29
|
renderEntityList([], 0, 0);
|
|
26
30
|
});
|
|
27
31
|
}
|
|
@@ -8,56 +8,123 @@
|
|
|
8
8
|
var REFRESH_INTERVAL = 5000; // 5 seconds
|
|
9
9
|
var refreshTimer = null;
|
|
10
10
|
|
|
11
|
+
// ── Install-token bootstrap (mirrors brain.js) ───────────────
|
|
12
|
+
// Dashboard callers authenticate with the install token so we never
|
|
13
|
+
// embed the mesh secret in JS. Token is fetched from /internal/token
|
|
14
|
+
// (loopback-only endpoint) and cached in sessionStorage for the tab session.
|
|
15
|
+
var TOKEN_STORAGE_KEY = 'slm_install_token';
|
|
16
|
+
|
|
17
|
+
function readToken() {
|
|
18
|
+
try { return window.sessionStorage ? window.sessionStorage.getItem(TOKEN_STORAGE_KEY) : null; }
|
|
19
|
+
catch (e) { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeToken(value) {
|
|
23
|
+
try { if (window.sessionStorage) window.sessionStorage.setItem(TOKEN_STORAGE_KEY, value); }
|
|
24
|
+
catch (e) { /* storage disabled */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fetchTokenFromServer() {
|
|
28
|
+
return fetch('/internal/token', { credentials: 'same-origin' })
|
|
29
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
30
|
+
.then(function(data) {
|
|
31
|
+
var tok = data && typeof data.token === 'string' ? data.token.trim() : '';
|
|
32
|
+
if (tok) { writeToken(tok); return tok; }
|
|
33
|
+
return null;
|
|
34
|
+
})
|
|
35
|
+
.catch(function() { return null; });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureToken() {
|
|
39
|
+
var cached = readToken();
|
|
40
|
+
if (cached) return Promise.resolve(cached);
|
|
41
|
+
return fetchTokenFromServer();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Authenticated fetch: injects X-Install-Token; retries once on 401
|
|
45
|
+
// (token may have been rotated since the tab was opened).
|
|
46
|
+
function meshFetch(url) {
|
|
47
|
+
return ensureToken().then(function(token) {
|
|
48
|
+
var opts = token ? { headers: { 'X-Install-Token': token }, credentials: 'same-origin' } : {};
|
|
49
|
+
return fetch(url, opts).then(function(r) {
|
|
50
|
+
if (r.status === 401) {
|
|
51
|
+
return fetchTokenFromServer().then(function(freshToken) {
|
|
52
|
+
var retryOpts = freshToken
|
|
53
|
+
? { headers: { 'X-Install-Token': freshToken }, credentials: 'same-origin' }
|
|
54
|
+
: {};
|
|
55
|
+
return fetch(url, retryOpts);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return r;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Public entry point ────────────────────────────────────────
|
|
11
64
|
window.loadMeshPeers = function() {
|
|
12
65
|
fetchMeshStatus();
|
|
13
66
|
fetchMeshPeers();
|
|
14
67
|
fetchMeshEvents();
|
|
15
68
|
fetchMeshState();
|
|
16
69
|
|
|
17
|
-
// Auto-refresh while tab is active
|
|
70
|
+
// Auto-refresh while tab is active; stop when pane loses focus
|
|
18
71
|
clearInterval(refreshTimer);
|
|
19
72
|
refreshTimer = setInterval(function() {
|
|
20
73
|
var pane = document.getElementById('mesh-pane');
|
|
21
74
|
if (pane && pane.classList.contains('active')) {
|
|
22
75
|
fetchMeshStatus();
|
|
23
76
|
fetchMeshPeers();
|
|
77
|
+
fetchMeshEvents();
|
|
78
|
+
fetchMeshState();
|
|
79
|
+
} else {
|
|
80
|
+
// Pane is no longer active — stop the timer to avoid background polling
|
|
81
|
+
clearInterval(refreshTimer);
|
|
82
|
+
refreshTimer = null;
|
|
24
83
|
}
|
|
25
84
|
}, REFRESH_INTERVAL);
|
|
26
85
|
};
|
|
27
86
|
|
|
28
87
|
// Try BOTH brokers: daemon (port 8765 /mesh/*) AND standalone slm-mesh (port 7899 /*)
|
|
29
|
-
var STANDALONE_PORT = null;
|
|
88
|
+
var STANDALONE_PORT = null;
|
|
89
|
+
|
|
90
|
+
function fetchStandaloneBroker(path) {
|
|
91
|
+
var ports = [7899];
|
|
92
|
+
return fetch('http://127.0.0.1:' + ports[0] + path, { signal: AbortSignal.timeout(2000) })
|
|
93
|
+
.then(function(r) {
|
|
94
|
+
if (r.ok) { STANDALONE_PORT = ports[0]; return r.json(); }
|
|
95
|
+
STANDALONE_PORT = null;
|
|
96
|
+
return null;
|
|
97
|
+
})
|
|
98
|
+
.catch(function() { STANDALONE_PORT = null; return null; });
|
|
99
|
+
}
|
|
30
100
|
|
|
31
101
|
function fetchMeshStatus() {
|
|
32
|
-
// Try daemon broker first, then standalone
|
|
33
102
|
Promise.all([
|
|
34
|
-
|
|
103
|
+
meshFetch('/mesh/status').then(function(r) {
|
|
104
|
+
if (!r.ok) return r.status === 401 ? { _auth_error: true } : null;
|
|
105
|
+
return r.json();
|
|
106
|
+
}).catch(function() { return null; }),
|
|
35
107
|
fetchStandaloneBroker('/health')
|
|
36
108
|
]).then(function(results) {
|
|
37
109
|
var daemon = results[0];
|
|
38
|
-
|
|
39
|
-
renderMeshStatus(daemon,
|
|
110
|
+
if (daemon && daemon._auth_error) { renderMeshStatusAuthError(); return; }
|
|
111
|
+
renderMeshStatus(daemon, results[1]);
|
|
40
112
|
});
|
|
41
113
|
}
|
|
42
114
|
|
|
43
|
-
function fetchStandaloneBroker(path) {
|
|
44
|
-
// Try port file first, then default 7899
|
|
45
|
-
var ports = [7899];
|
|
46
|
-
return fetch('http://127.0.0.1:' + ports[0] + path, { signal: AbortSignal.timeout(2000) })
|
|
47
|
-
.then(function(r) { STANDALONE_PORT = ports[0]; return r.json(); })
|
|
48
|
-
.catch(function() { return null; });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
115
|
function fetchMeshPeers() {
|
|
52
|
-
// Fetch from BOTH brokers and merge
|
|
53
116
|
Promise.all([
|
|
54
|
-
|
|
117
|
+
meshFetch('/mesh/peers').then(function(r) {
|
|
118
|
+
if (!r.ok) return r.status === 401 ? { _auth_error: true } : { peers: [] };
|
|
119
|
+
return r.json();
|
|
120
|
+
}).catch(function() { return { peers: [] }; }),
|
|
55
121
|
fetchStandaloneBroker('/peers')
|
|
56
122
|
]).then(function(results) {
|
|
57
|
-
var
|
|
123
|
+
var daemonResult = results[0];
|
|
124
|
+
if (daemonResult && daemonResult._auth_error) { renderMeshPeersAuthError(); return; }
|
|
125
|
+
var daemonPeers = (daemonResult && daemonResult.peers) || [];
|
|
58
126
|
var standalonePeers = (results[1] && (results[1].peers || results[1])) || [];
|
|
59
127
|
if (!Array.isArray(standalonePeers)) standalonePeers = [];
|
|
60
|
-
// Merge, dedup by peer_id
|
|
61
128
|
var seen = {};
|
|
62
129
|
var allPeers = [];
|
|
63
130
|
daemonPeers.concat(standalonePeers).forEach(function(p) {
|
|
@@ -66,7 +133,8 @@
|
|
|
66
133
|
});
|
|
67
134
|
renderMeshPeers(allPeers);
|
|
68
135
|
}).catch(function() {
|
|
69
|
-
document.getElementById('mesh-peers-list')
|
|
136
|
+
var el = document.getElementById('mesh-peers-list');
|
|
137
|
+
if (el) el.innerHTML =
|
|
70
138
|
'<div class="text-center" style="padding:24px;color:var(--ng-text-tertiary)">' +
|
|
71
139
|
'<i class="bi bi-wifi-off" style="font-size:2rem;display:block;margin-bottom:8px"></i>' +
|
|
72
140
|
'Mesh broker not reachable' +
|
|
@@ -75,17 +143,51 @@
|
|
|
75
143
|
}
|
|
76
144
|
|
|
77
145
|
function fetchMeshEvents() {
|
|
78
|
-
|
|
146
|
+
meshFetch('/mesh/events').then(function(r) {
|
|
147
|
+
if (!r.ok) return { events: [] };
|
|
148
|
+
return r.json();
|
|
149
|
+
}).then(function(data) {
|
|
79
150
|
renderMeshEvents(data.events || data || []);
|
|
80
151
|
}).catch(function() {});
|
|
81
152
|
}
|
|
82
153
|
|
|
83
154
|
function fetchMeshState() {
|
|
84
|
-
|
|
155
|
+
meshFetch('/mesh/state').then(function(r) {
|
|
156
|
+
if (!r.ok) return { state: {} };
|
|
157
|
+
return r.json();
|
|
158
|
+
}).then(function(data) {
|
|
85
159
|
renderMeshState(data.state || data || {});
|
|
86
160
|
}).catch(function() {});
|
|
87
161
|
}
|
|
88
162
|
|
|
163
|
+
// ── Auth-error renderers ──────────────────────────────────────
|
|
164
|
+
function renderMeshStatusAuthError() {
|
|
165
|
+
var el = document.getElementById('mesh-status-cards');
|
|
166
|
+
if (!el) return;
|
|
167
|
+
el.innerHTML =
|
|
168
|
+
'<div class="row g-3">' +
|
|
169
|
+
statusCard('Status', statusDot('error') + ' Auth required', 'bi-lock') +
|
|
170
|
+
statusCard('Peers', '—', 'bi-people') +
|
|
171
|
+
statusCard('Uptime', '—', 'bi-clock') +
|
|
172
|
+
statusCard('Brokers', '—', 'bi-hdd-stack') +
|
|
173
|
+
'</div>' +
|
|
174
|
+
'<div style="font-size:0.75rem;color:var(--ng-status-error);margin-top:8px;text-align:center">' +
|
|
175
|
+
'Mesh endpoints returned 401. The daemon may be starting — retrying automatically.' +
|
|
176
|
+
'</div>';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderMeshPeersAuthError() {
|
|
180
|
+
var el = document.getElementById('mesh-peers-list');
|
|
181
|
+
if (!el) return;
|
|
182
|
+
el.innerHTML =
|
|
183
|
+
'<div class="text-center" style="padding:32px;color:var(--ng-text-tertiary)">' +
|
|
184
|
+
'<i class="bi bi-lock" style="font-size:2.5rem;display:block;margin-bottom:12px;opacity:0.5"></i>' +
|
|
185
|
+
'<div style="font-size:0.9375rem;margin-bottom:4px">Authentication required</div>' +
|
|
186
|
+
'<div style="font-size:0.8125rem">Could not authenticate with the mesh broker. ' +
|
|
187
|
+
'The dashboard token may still be loading.</div>' +
|
|
188
|
+
'</div>';
|
|
189
|
+
}
|
|
190
|
+
|
|
89
191
|
function renderMeshStatus(daemonData, standaloneData) {
|
|
90
192
|
var el = document.getElementById('mesh-status-cards');
|
|
91
193
|
if (!el) return;
|
|
@@ -130,13 +130,18 @@
|
|
|
130
130
|
|
|
131
131
|
async function _putConfig(body) {
|
|
132
132
|
try {
|
|
133
|
-
await fetch('/api/optimize/config', {
|
|
133
|
+
var resp = await fetch('/api/optimize/config', {
|
|
134
134
|
method: 'PUT',
|
|
135
135
|
headers: {'Content-Type': 'application/json'},
|
|
136
136
|
body: JSON.stringify(body)
|
|
137
137
|
});
|
|
138
|
+
if (!resp.ok) {
|
|
139
|
+
console.warn('Config update failed: HTTP ' + resp.status);
|
|
140
|
+
_loadOptimizeConfig(); // revert toggles to server state
|
|
141
|
+
}
|
|
138
142
|
} catch (e) {
|
|
139
143
|
console.log('Config update error:', e);
|
|
144
|
+
_loadOptimizeConfig(); // revert toggles to server state
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -210,7 +210,7 @@ async function switchProfile(profileName) {
|
|
|
210
210
|
showToast('Switched to profile: ' + profileName);
|
|
211
211
|
loadProfiles();
|
|
212
212
|
loadStats();
|
|
213
|
-
loadGraph();
|
|
213
|
+
if (typeof loadGraph === 'function') loadGraph();
|
|
214
214
|
loadProfilesTable();
|
|
215
215
|
// v2.7.4: Reload ALL tabs for new profile
|
|
216
216
|
if (typeof loadLearning === 'function') loadLearning();
|
|
@@ -9,7 +9,9 @@ var recallLabState = {
|
|
|
9
9
|
synthesis: '',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Delegation handles the case where the recall-lab tab pane is injected after parse time.
|
|
13
|
+
document.addEventListener('click', function(e) {
|
|
14
|
+
if (!e.target || e.target.id !== 'recall-lab-search') return;
|
|
13
15
|
var query = document.getElementById('recall-lab-query').value.trim();
|
|
14
16
|
if (!query) return;
|
|
15
17
|
|
|
@@ -287,6 +289,10 @@ function buildChannelBar(name, score, max, color) {
|
|
|
287
289
|
return row;
|
|
288
290
|
}
|
|
289
291
|
|
|
290
|
-
document.
|
|
291
|
-
if (e.
|
|
292
|
+
document.addEventListener('keydown', function(e) {
|
|
293
|
+
if (!e.target || e.target.id !== 'recall-lab-query') return;
|
|
294
|
+
if (e.key === 'Enter') {
|
|
295
|
+
var btn = document.getElementById('recall-lab-search');
|
|
296
|
+
if (btn) btn.click();
|
|
297
|
+
}
|
|
292
298
|
});
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
async function loadTimeline() {
|
|
5
5
|
showLoading('timeline-chart', 'Loading timeline...');
|
|
6
6
|
try {
|
|
7
|
-
var response = await fetch('/api/timeline
|
|
7
|
+
var response = await fetch('/api/v3/timeline/?range=30d&group_by=date&limit=1000');
|
|
8
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
8
9
|
var data = await response.json();
|
|
9
|
-
renderTimeline(data.timeline);
|
|
10
|
+
renderTimeline(data.events || data.timeline);
|
|
10
11
|
} catch (error) {
|
|
11
12
|
console.error('Error loading timeline:', error);
|
|
12
13
|
showEmpty('timeline-chart', 'clock-history', 'Failed to load timeline');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superlocalmemory
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.22
|
|
4
4
|
Summary: Information-geometric agent memory with mathematical guarantees
|
|
5
5
|
Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -96,10 +96,10 @@ Dynamic: license-file
|
|
|
96
96
|
<img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory" width="200"/>
|
|
97
97
|
</p>
|
|
98
98
|
|
|
99
|
-
<h1 align="center">SuperLocalMemory V3.6.
|
|
99
|
+
<h1 align="center">SuperLocalMemory V3.6.21</h1>
|
|
100
100
|
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
|
|
101
101
|
<em>To the best of our knowledge, the only zero-cloud agent memory that beats Mem0's zero-LLM score on LoCoMo. Mode A: 74.8% vs Mem0 64.2% — no GPU, no API key, on CPU.</em></p>
|
|
102
|
-
<p align="center"><code>v3.6.
|
|
102
|
+
<p align="center"><code>v3.6.21</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
|
|
103
103
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
104
104
|
<p align="center"><strong>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>
|
|
105
105
|
|
|
@@ -401,6 +401,8 @@ slm dashboard # Opens at http://localhost:8765
|
|
|
401
401
|
|
|
402
402
|
| Version | Codename | Key Features |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v3.6.21** | Dashboard Audit | Full UI audit across all 7 dashboard tabs — auth fix for mesh panel (issue #60 frontend), Quick Store endpoint, timeline endpoint, r.ok guards, SSE \r fix, event delegation for lazy tabs, optimize toggle revert; backbone.py JSONDecodeError fix (issue #62) |
|
|
405
|
+
| **v3.6.20** | Mesh Auth | Remote mesh auth fix (issue #60) — `_get_broker` now accepts Bearer + X-Mesh-Secret from non-loopback callers; config settings preservation (AIDEV-86) |
|
|
404
406
|
| **v3.6.17** | Community | 8 contributor PRs (observability events, marker-bounded adapter writes, daemon port discovery, anthropic `api_base`, OpenMP workers, atomic-write rehash, `_jl` sentinel, LFS pointer); dashboard-feedback fix (#53/#59); env-tunable SQLite knobs + idle backoff; remote LLM test-probe (#40) |
|
|
405
407
|
| **v3.6.16** | Docs | Corrected Claude Code plugin install — adds the required `/plugin marketplace add` step; clarifies plugin vs pip/npm delivery |
|
|
406
408
|
| **v3.6.15** | Multi-scope | **Opt-in [shared memory](docs/shared-memory.md)** (personal/shared/global, off by default), default-deny scope at every read path, recall scope-race fix, contributor PRs #42/#43/#44, fixes #46–#49 |
|