superlocalmemory 3.4.22 → 3.4.23
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 +29 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/slm-build-graph/SKILL.md +1 -1
- package/skills/slm-list-recent/SKILL.md +1 -1
- package/skills/slm-recall/SKILL.md +1 -1
- package/skills/slm-remember/SKILL.md +1 -1
- package/skills/slm-status/SKILL.md +1 -1
- package/skills/slm-switch-profile/SKILL.md +1 -1
- package/src/superlocalmemory/__init__.py +3 -0
- package/src/superlocalmemory/core/context_cache.py +1 -1
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/learning/database.py +1 -1
- package/src/superlocalmemory/server/routes/brain.py +1 -1
- package/src/superlocalmemory/server/security_middleware.py +20 -2
- package/src/superlocalmemory/server/unified_daemon.py +107 -5
- package/src/superlocalmemory/ui/index.html +4 -0
- package/src/superlocalmemory/ui/js/core.js +96 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +655 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +426 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +58 -0
- package/src/superlocalmemory.egg-info/top_level.txt +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [3.4.23] - 2026-04-21
|
|
14
|
+
|
|
15
|
+
Critical hotfix on top of 3.4.22 for two end-user-facing regressions.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Daemon error log no longer balloons.** A ternary passed as the
|
|
19
|
+
`logger.info` format string caused a `TypeError` on every startup in 24/7
|
|
20
|
+
mode. Python's logging module then dumped the full FastAPI
|
|
21
|
+
`merged_lifespan` stack to stderr; over a day the LaunchAgent log grew to
|
|
22
|
+
tens of MB. The call is now pre-formatted. A defensive log-rotation pass
|
|
23
|
+
at startup truncates any daemon log over 10 MB so users upgrading from
|
|
24
|
+
3.4.22 get a clean slate on first boot.
|
|
25
|
+
- **Dashboard no longer hangs after a daemon upgrade.** Static JS/CSS/HTML
|
|
26
|
+
was served without cache headers, so browsers served stale modules after
|
|
27
|
+
`slm restart` and the dashboard showed an infinite spinner. All static
|
|
28
|
+
responses now ship `Cache-Control: no-cache, must-revalidate`, and
|
|
29
|
+
`index.html` embeds the server version; on mismatch the tab clears
|
|
30
|
+
`localStorage` (preserving theme) and hard-reloads once.
|
|
31
|
+
- **Fetches can no longer hang forever.** A global `fetch` patch attaches a
|
|
32
|
+
15-second `AbortController` timeout to every relative-URL request, so a
|
|
33
|
+
dead socket surfaces as a rejection instead of leaving a spinner
|
|
34
|
+
spinning. No callsite changes required.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `GET /api/version` — returns the running daemon version; consumed by the
|
|
38
|
+
dashboard version-fingerprint auto-reload.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
13
42
|
## [3.4.22] - 2026-04-18
|
|
14
43
|
|
|
15
44
|
Hardening release — correctness, stability, and security fixes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.23",
|
|
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,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slm-build-graph
|
|
3
3
|
description: Build or rebuild the knowledge graph from existing memories using TF-IDF entity extraction and Leiden clustering. Use when search results seem poor, after bulk imports, or to optimize performance. Automatically discovers relationships between memories and creates topic clusters.
|
|
4
|
-
version: "3.4.
|
|
4
|
+
version: "3.4.23"
|
|
5
5
|
license: AGPL-3.0-or-later
|
|
6
6
|
compatibility: "Requires SuperLocalMemory V2 installed at ~/.claude-memory/, optional dependencies: python-igraph, leidenalg"
|
|
7
7
|
attribution:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slm-list-recent
|
|
3
3
|
description: List most recent memories in chronological order. Use when the user wants to see what was recently saved, review recent conversations, check what they worked on today, or browse memory history. Shows memories sorted by creation time (newest first).
|
|
4
|
-
version: "3.4.
|
|
4
|
+
version: "3.4.23"
|
|
5
5
|
license: AGPL-3.0-or-later
|
|
6
6
|
compatibility: "Requires SuperLocalMemory V2 installed at ~/.claude-memory/"
|
|
7
7
|
attribution:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slm-recall
|
|
3
3
|
description: Search SuperLocalMemory for relevant facts, decisions, and past context. Use when the user asks to recall, search, find, or retrieve stored information. Invokes 5-channel retrieval with LightGBM reranking via MCP.
|
|
4
|
-
version: "3.4.
|
|
4
|
+
version: "3.4.23"
|
|
5
5
|
license: AGPL-3.0-or-later
|
|
6
6
|
compatibility: "SuperLocalMemory v3.4.22 — MCP (preferred) or CLI fallback"
|
|
7
7
|
attribution:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slm-remember
|
|
3
3
|
description: Save content to SuperLocalMemory with intelligent indexing and knowledge graph integration. Use when the user wants to remember information, save context, store coding decisions, or persist knowledge for future sessions. Automatically indexes, graphs, and learns patterns.
|
|
4
|
-
version: "3.4.
|
|
4
|
+
version: "3.4.23"
|
|
5
5
|
license: AGPL-3.0-or-later
|
|
6
6
|
compatibility: "Requires SuperLocalMemory V2 installed at ~/.claude-memory/"
|
|
7
7
|
attribution:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slm-status
|
|
3
3
|
description: Check SuperLocalMemory system status, health, and statistics. Use when the user wants to know memory count, graph stats, patterns learned, database health, or system diagnostics. Shows comprehensive system health dashboard.
|
|
4
|
-
version: "3.4.
|
|
4
|
+
version: "3.4.23"
|
|
5
5
|
license: AGPL-3.0-or-later
|
|
6
6
|
compatibility: "Requires SuperLocalMemory V2 installed at ~/.claude-memory/"
|
|
7
7
|
attribution:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slm-switch-profile
|
|
3
3
|
description: Switch between memory profiles for context isolation and management. Use when the user wants to change profile context, separate work/personal memories, or manage multiple independent memory spaces. Each profile has its own database, graph, and patterns.
|
|
4
|
-
version: "3.4.
|
|
4
|
+
version: "3.4.23"
|
|
5
5
|
license: AGPL-3.0-or-later
|
|
6
6
|
compatibility: "Requires SuperLocalMemory V2 installed at ~/.claude-memory/"
|
|
7
7
|
attribution:
|
|
@@ -47,7 +47,7 @@ TTL_SECONDS: int = 120
|
|
|
47
47
|
CLEANUP_HORIZON_SECONDS: int = 600
|
|
48
48
|
MAX_BYTES: int = 50 * 1024 * 1024
|
|
49
49
|
MAX_CONTENT_CHARS: int = 4000
|
|
50
|
-
SCHEMA_VERSION: str = "3.4.
|
|
50
|
+
SCHEMA_VERSION: str = "3.4.23"
|
|
51
51
|
|
|
52
52
|
_HMAC_MATERIAL: bytes = b"active_brain_cache"
|
|
53
53
|
_HMAC_HEX_LEN: int = 32
|
|
@@ -64,7 +64,7 @@ router = APIRouter(prefix="/api/v3", tags=["brain"])
|
|
|
64
64
|
# LLD-03 v2 stratum space = 4 query types × 3 entity bins × 4 time buckets.
|
|
65
65
|
_STRATA_TOTAL: int = 48
|
|
66
66
|
|
|
67
|
-
_VERSION: str = "3.4.
|
|
67
|
+
_VERSION: str = "3.4.23"
|
|
68
68
|
|
|
69
69
|
# Banned metric names (LLD-04 U4). Kept as a tuple for grep visibility;
|
|
70
70
|
# the source-level test asserts we don't accidentally reintroduce them.
|
|
@@ -56,9 +56,27 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
56
56
|
# Control referrer information leakage
|
|
57
57
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
58
58
|
|
|
59
|
-
#
|
|
60
|
-
|
|
59
|
+
# v3.4.23: Cache-Control strategy
|
|
60
|
+
# ---------------------------------------------------------------
|
|
61
|
+
# Three classes of paths, three policies:
|
|
62
|
+
#
|
|
63
|
+
# /api/* -> no-store (sensitive data, never cache)
|
|
64
|
+
# index.html -> no-cache, must-revalidate (always revalidate)
|
|
65
|
+
# /static/* -> no-cache, must-revalidate (always revalidate
|
|
66
|
+
# with ETag; fast reloads but never stale-after-
|
|
67
|
+
# upgrade)
|
|
68
|
+
#
|
|
69
|
+
# Before v3.4.23 only /api/* had cache headers. Browsers then cached
|
|
70
|
+
# JS/CSS/HTML aggressively via default heuristics, and after a daemon
|
|
71
|
+
# upgrade the dashboard showed an infinite spinner because old cached
|
|
72
|
+
# JS was calling endpoints with stale response shapes. "no-cache"
|
|
73
|
+
# (not "no-store") still allows 304s on unchanged files, so reload
|
|
74
|
+
# cost stays low.
|
|
75
|
+
path = request.url.path
|
|
76
|
+
if path.startswith("/api/"):
|
|
61
77
|
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
|
62
78
|
response.headers["Pragma"] = "no-cache"
|
|
79
|
+
elif path == "/" or path.endswith(".html") or path.startswith("/static/"):
|
|
80
|
+
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
|
63
81
|
|
|
64
82
|
return response
|
|
@@ -495,9 +495,20 @@ async def lifespan(application: FastAPI):
|
|
|
495
495
|
global _start_time
|
|
496
496
|
_start_time = time.monotonic()
|
|
497
497
|
_last_activity = time.monotonic()
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
# v3.4.23: pre-format the ready message. Previous code passed a ternary as
|
|
499
|
+
# the log format string with a fixed 2-arg tuple; when idle_timeout<=0 the
|
|
500
|
+
# chosen branch had only one %d, triggering a TypeError on every startup.
|
|
501
|
+
# Python's logging module then wrote the full stack to stderr. Because the
|
|
502
|
+
# call runs inside FastAPI's stacked merged_lifespan, each dump was ~30 KB
|
|
503
|
+
# and the error log grew to tens of MB within a day.
|
|
504
|
+
if idle_timeout <= 0:
|
|
505
|
+
_ready_msg = f"Unified daemon ready on port {_DEFAULT_PORT} (24/7 mode)"
|
|
506
|
+
else:
|
|
507
|
+
_ready_msg = (
|
|
508
|
+
f"Unified daemon ready on port {_DEFAULT_PORT} "
|
|
509
|
+
f"(idle timeout: {idle_timeout}s)"
|
|
510
|
+
)
|
|
511
|
+
logger.info(_ready_msg)
|
|
501
512
|
|
|
502
513
|
yield
|
|
503
514
|
|
|
@@ -850,7 +861,18 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
850
861
|
_data_io_mod.ws_manager = ws_manager
|
|
851
862
|
|
|
852
863
|
# Root page
|
|
853
|
-
from fastapi.responses import HTMLResponse
|
|
864
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
865
|
+
|
|
866
|
+
# v3.4.23: /api/version — dashboard polls this to detect daemon upgrades
|
|
867
|
+
# and auto-reload stale tabs (see ui/js/core.js::checkVersionFingerprint).
|
|
868
|
+
try:
|
|
869
|
+
from superlocalmemory import __version__ as _SLM_VERSION
|
|
870
|
+
except Exception: # pragma: no cover — defensive
|
|
871
|
+
_SLM_VERSION = "unknown"
|
|
872
|
+
|
|
873
|
+
@application.get("/api/version")
|
|
874
|
+
async def api_version():
|
|
875
|
+
return JSONResponse({"version": _SLM_VERSION})
|
|
854
876
|
|
|
855
877
|
@application.get("/", response_class=HTMLResponse)
|
|
856
878
|
async def root():
|
|
@@ -863,7 +885,11 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
863
885
|
"<p><a href='/docs'>API Documentation</a></p>"
|
|
864
886
|
"</body></html>"
|
|
865
887
|
)
|
|
866
|
-
|
|
888
|
+
# v3.4.23: substitute version placeholder so the dashboard can detect
|
|
889
|
+
# upgrades and auto-reload. Read fresh each request (daemon uptime is
|
|
890
|
+
# days, but we want zero caching surprises during development).
|
|
891
|
+
html = index_path.read_text()
|
|
892
|
+
return html.replace("__SLM_VERSION__", _SLM_VERSION)
|
|
867
893
|
|
|
868
894
|
# Startup event for event listener
|
|
869
895
|
@application.on_event("startup")
|
|
@@ -1066,6 +1092,13 @@ def start_server(port: int = _DEFAULT_PORT) -> None:
|
|
|
1066
1092
|
global _start_time
|
|
1067
1093
|
import uvicorn
|
|
1068
1094
|
|
|
1095
|
+
# v3.4.23: rotate oversized logs before anything else so both the CLI
|
|
1096
|
+
# path (`slm serve`) and the LaunchAgent path (__main__) are covered.
|
|
1097
|
+
try:
|
|
1098
|
+
rotate_oversized_logs()
|
|
1099
|
+
except Exception:
|
|
1100
|
+
pass # never block startup on log housekeeping
|
|
1101
|
+
|
|
1069
1102
|
_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
1070
1103
|
_PID_FILE.write_text(str(os.getpid()))
|
|
1071
1104
|
_PORT_FILE.write_text(str(port))
|
|
@@ -1094,11 +1127,80 @@ def start_server(port: int = _DEFAULT_PORT) -> None:
|
|
|
1094
1127
|
_PORT_FILE.unlink(missing_ok=True)
|
|
1095
1128
|
|
|
1096
1129
|
|
|
1130
|
+
# ---------------------------------------------------------------------------
|
|
1131
|
+
# v3.4.23 — Startup log rotation
|
|
1132
|
+
# ---------------------------------------------------------------------------
|
|
1133
|
+
# The LaunchAgent plist redirects stdout/stderr to daemon.log and
|
|
1134
|
+
# daemon-error.log. Those files are managed by launchd, not Python, so
|
|
1135
|
+
# Python's RotatingFileHandler cannot prune them. If any bug ever writes
|
|
1136
|
+
# large amounts of data to stderr (the v3.4.22 logger-format bug produced
|
|
1137
|
+
# ~30 KB per startup and the file grew to 69 MB), end users end up with a
|
|
1138
|
+
# disk-eating log they never knew existed.
|
|
1139
|
+
#
|
|
1140
|
+
# rotate_oversized_logs() is a belt-and-suspenders guard: every time the
|
|
1141
|
+
# daemon starts, if either log exceeds MAX_LOG_BYTES we rename the current
|
|
1142
|
+
# file to ".1" (keeping one rotated copy) and truncate the original so
|
|
1143
|
+
# launchd's open file descriptor keeps working. This is cheap, stateless,
|
|
1144
|
+
# and independent of whatever caused the overflow.
|
|
1145
|
+
# ---------------------------------------------------------------------------
|
|
1146
|
+
|
|
1147
|
+
_MAX_LOG_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def rotate_oversized_logs(log_dir: Optional[Path] = None,
|
|
1151
|
+
max_bytes: int = _MAX_LOG_BYTES) -> None:
|
|
1152
|
+
"""Rotate daemon.log and daemon-error.log at startup if oversized.
|
|
1153
|
+
|
|
1154
|
+
Keeps one rotated copy (.1). Safe under concurrent start attempts:
|
|
1155
|
+
rename is atomic on POSIX, and truncation is idempotent.
|
|
1156
|
+
"""
|
|
1157
|
+
log_dir = log_dir or (Path.home() / ".superlocalmemory" / "logs")
|
|
1158
|
+
try:
|
|
1159
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
1160
|
+
except Exception:
|
|
1161
|
+
return
|
|
1162
|
+
for name in ("daemon.log", "daemon-error.log", "daemon.json.log"):
|
|
1163
|
+
path = log_dir / name
|
|
1164
|
+
try:
|
|
1165
|
+
if not path.exists() or path.stat().st_size <= max_bytes:
|
|
1166
|
+
continue
|
|
1167
|
+
rotated = log_dir / f"{name}.1"
|
|
1168
|
+
try:
|
|
1169
|
+
if rotated.exists():
|
|
1170
|
+
rotated.unlink()
|
|
1171
|
+
except Exception:
|
|
1172
|
+
pass
|
|
1173
|
+
try:
|
|
1174
|
+
path.rename(rotated)
|
|
1175
|
+
except Exception:
|
|
1176
|
+
# If rename fails (e.g., file is the open stderr fd under
|
|
1177
|
+
# launchd), fall back to truncation so we at least reclaim
|
|
1178
|
+
# disk without breaking the redirect.
|
|
1179
|
+
try:
|
|
1180
|
+
with open(path, "w"):
|
|
1181
|
+
pass
|
|
1182
|
+
except Exception:
|
|
1183
|
+
pass
|
|
1184
|
+
continue
|
|
1185
|
+
# Re-create the original path as empty so launchd's redirect
|
|
1186
|
+
# keeps appending to a fresh file.
|
|
1187
|
+
try:
|
|
1188
|
+
path.touch()
|
|
1189
|
+
except Exception:
|
|
1190
|
+
pass
|
|
1191
|
+
except Exception:
|
|
1192
|
+
# Log rotation must never prevent daemon startup.
|
|
1193
|
+
continue
|
|
1194
|
+
|
|
1195
|
+
|
|
1097
1196
|
# ---------------------------------------------------------------------------
|
|
1098
1197
|
# CLI entry point
|
|
1099
1198
|
# ---------------------------------------------------------------------------
|
|
1100
1199
|
|
|
1101
1200
|
if __name__ == "__main__":
|
|
1201
|
+
# Rotate first, then configure logging, so the first log line lands in a
|
|
1202
|
+
# freshly-sized file.
|
|
1203
|
+
rotate_oversized_logs()
|
|
1102
1204
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
|
|
1103
1205
|
port = _DEFAULT_PORT
|
|
1104
1206
|
for arg in sys.argv:
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<!-- v3.4.23: server substitutes __SLM_VERSION__ at serve time. core.js
|
|
7
|
+
compares this to /api/version and hard-reloads + clears localStorage
|
|
8
|
+
on mismatch, so the browser cannot show stale UI after an upgrade. -->
|
|
9
|
+
<meta name="slm-version" content="__SLM_VERSION__">
|
|
6
10
|
<title>SuperLocalMemory V3 — Dashboard</title>
|
|
7
11
|
|
|
8
12
|
<!-- Bootstrap CSS (vendored locally v3.4.21 — no CDN calls, works offline) -->
|
|
@@ -3,6 +3,97 @@
|
|
|
3
3
|
// Security: All dynamic text MUST pass through escapeHtml() before DOM insertion.
|
|
4
4
|
// Data originates from our own trusted local SQLite database (localhost only).
|
|
5
5
|
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// v3.4.23 — slmFetch(): fetch with 15s abort timeout
|
|
8
|
+
// ----------------------------------------------------------------------------
|
|
9
|
+
// Bare fetch() never resolves when the daemon dies mid-request (socket kept
|
|
10
|
+
// open, Promise pending). That leaves dashboard spinners running forever and
|
|
11
|
+
// stacks up orphan fetches that make hard-refresh hang. slmFetch wraps every
|
|
12
|
+
// request in an AbortController with a 15 s ceiling, so a dead daemon
|
|
13
|
+
// surfaces as a normal rejection and the UI can show a clear error.
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
window.SLM_FETCH_TIMEOUT_MS = 15000;
|
|
17
|
+
|
|
18
|
+
// Global fetch patch: apply the abort timeout to every relative-URL request
|
|
19
|
+
// automatically. 17 UI modules call bare fetch() — patching here avoids
|
|
20
|
+
// touching each one and guarantees no future callsite can regress to an
|
|
21
|
+
// un-timed fetch that holds the spinner forever. Absolute URLs (external
|
|
22
|
+
// resources) are passed through unchanged. Callers that already supply
|
|
23
|
+
// `signal` keep their own behavior. `init.timeoutMs` lets callers override
|
|
24
|
+
// the default per-request.
|
|
25
|
+
(function patchFetch() {
|
|
26
|
+
if (window.__slmFetchPatched) return;
|
|
27
|
+
window.__slmFetchPatched = true;
|
|
28
|
+
var _origFetch = window.fetch.bind(window);
|
|
29
|
+
window.fetch = function (input, init) {
|
|
30
|
+
init = init || {};
|
|
31
|
+
var urlStr = typeof input === 'string' ? input : (input && input.url) || '';
|
|
32
|
+
var isRelative = !(/^https?:\/\//i.test(urlStr));
|
|
33
|
+
if (!isRelative || init.signal) {
|
|
34
|
+
return _origFetch(input, init);
|
|
35
|
+
}
|
|
36
|
+
var controller = new AbortController();
|
|
37
|
+
var timeoutMs = init.timeoutMs || window.SLM_FETCH_TIMEOUT_MS;
|
|
38
|
+
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
39
|
+
init.signal = controller.signal;
|
|
40
|
+
return _origFetch(input, init).finally(function () { clearTimeout(timer); });
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
|
|
44
|
+
// Thin named wrapper for callsites that want explicit timeout control.
|
|
45
|
+
// Equivalent to the patched fetch above but accepts `init.timeoutMs`.
|
|
46
|
+
async function slmFetch(input, init) {
|
|
47
|
+
return fetch(input, init || {});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// v3.4.23 — version fingerprint + auto-reload on daemon upgrade
|
|
52
|
+
// ----------------------------------------------------------------------------
|
|
53
|
+
// index.html ships with <meta name="slm-version" content="__SLM_VERSION__">
|
|
54
|
+
// that the server fills in at serve time. After page load we ask the daemon
|
|
55
|
+
// for its current version via /api/version; on mismatch we clear localStorage
|
|
56
|
+
// and hard-reload once, so a stale tab never lingers after `slm restart` or
|
|
57
|
+
// a package upgrade. Guarded by sessionStorage to avoid reload loops.
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
async function checkVersionFingerprint() {
|
|
61
|
+
try {
|
|
62
|
+
var metaEl = document.querySelector('meta[name="slm-version"]');
|
|
63
|
+
var pageVersion = metaEl ? metaEl.getAttribute('content') : null;
|
|
64
|
+
if (!pageVersion || pageVersion === '__SLM_VERSION__') return;
|
|
65
|
+
var resp = await slmFetch('/api/version', { timeoutMs: 5000 });
|
|
66
|
+
if (!resp.ok) return;
|
|
67
|
+
var data = await resp.json();
|
|
68
|
+
var serverVersion = data && data.version;
|
|
69
|
+
if (!serverVersion || serverVersion === pageVersion) return;
|
|
70
|
+
try {
|
|
71
|
+
if (sessionStorage.getItem('slm-version-reload-done') === serverVersion) {
|
|
72
|
+
console.warn('[slm] version mismatch persists after reload:',
|
|
73
|
+
pageVersion, '!=', serverVersion);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sessionStorage.setItem('slm-version-reload-done', serverVersion);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// sessionStorage blocked (private mode, quota, etc.) — fall through
|
|
79
|
+
// to reload. Worst case: we reload twice instead of once, still
|
|
80
|
+
// safe because server version converges on second attempt.
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
// Preserve theme; drop everything else that might be stale.
|
|
84
|
+
var theme = localStorage.getItem('slm-theme');
|
|
85
|
+
localStorage.clear();
|
|
86
|
+
if (theme) localStorage.setItem('slm-theme', theme);
|
|
87
|
+
} catch (e) { /* localStorage may be blocked */ }
|
|
88
|
+
console.info('[slm] daemon upgraded', pageVersion, '->', serverVersion,
|
|
89
|
+
'— reloading');
|
|
90
|
+
location.reload();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Network error or daemon down: don't reload, just log.
|
|
93
|
+
console.debug('[slm] version check skipped:', err && err.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
6
97
|
// ============================================================================
|
|
7
98
|
// Dark Mode
|
|
8
99
|
// ============================================================================
|
|
@@ -180,7 +271,7 @@ function formatDateFull(dateString) {
|
|
|
180
271
|
|
|
181
272
|
async function loadStats() {
|
|
182
273
|
try {
|
|
183
|
-
var response = await
|
|
274
|
+
var response = await slmFetch('/api/stats');
|
|
184
275
|
var data = await response.json();
|
|
185
276
|
var ov = data.overview || {};
|
|
186
277
|
animateCounter('stat-memories', ov.total_memories || 0);
|
|
@@ -235,6 +326,10 @@ function populateFilters(categories, projects) {
|
|
|
235
326
|
|
|
236
327
|
window.addEventListener('DOMContentLoaded', function() {
|
|
237
328
|
initDarkMode();
|
|
329
|
+
// v3.4.23: version check runs first and non-blocking. If a mismatch is
|
|
330
|
+
// detected it triggers location.reload(), so the rest of init on the
|
|
331
|
+
// stale page becomes a no-op.
|
|
332
|
+
checkVersionFingerprint();
|
|
238
333
|
loadProfiles();
|
|
239
334
|
loadStats();
|
|
240
335
|
loadGraph();
|