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 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.22",
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.22"
3
+ version = "3.4.23"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -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.22"
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.22"
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.22"
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.22"
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.22"
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.22"
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:
@@ -0,0 +1,3 @@
1
+ """SuperLocalMemory — information-geometric agent memory."""
2
+
3
+ __version__ = "3.4.23"
@@ -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.22"
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
@@ -22,7 +22,7 @@ from typing import Callable, Iterable
22
22
  from superlocalmemory.core.security_primitives import redact_secrets
23
23
 
24
24
 
25
- VERSION = "3.4.22"
25
+ VERSION = "3.4.23"
26
26
  DEFAULT_TOP_K = 10
27
27
  DEFAULT_DECISIONS_K = 5
28
28
  DEFAULT_MEMORIES_K = 10
@@ -395,7 +395,7 @@ class LearningDatabase:
395
395
  feature_names: list[str],
396
396
  trained_on_count: int,
397
397
  metrics: dict,
398
- model_version: str = "3.4.22",
398
+ model_version: str = "3.4.23",
399
399
  ) -> int:
400
400
  """Persist a newly trained model and flip the active flag.
401
401
 
@@ -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.22"
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
- # Prevent caching of sensitive data (for API endpoints)
60
- if request.url.path.startswith("/api/"):
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
- logger.info("Unified daemon ready on port %d (24/7 mode)" if idle_timeout <= 0
499
- else "Unified daemon ready on port %d (idle timeout: %ds)",
500
- _DEFAULT_PORT, idle_timeout)
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
- return index_path.read_text()
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 fetch('/api/stats');
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();